diff --git a/Directory.Packages.props b/Directory.Packages.props index bc0a2db4fd..1b350a789e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,25 +14,25 @@ - - - + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -46,22 +46,22 @@ - - + + - + - + - - - - + + + + @@ -73,14 +73,14 @@ - + - + @@ -91,7 +91,7 @@ - + diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 966c4b982e..781e2e69fd 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -623,13 +623,24 @@ stages: displayName: Copy Playwright results condition: succeededOrFailed() - # Publish + # Publish test artifacts - task: PublishPipelineArtifact@1 displayName: Publish test artifacts condition: succeededOrFailed() inputs: targetPath: $(Build.ArtifactStagingDirectory) artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)" + + # Publish test results + - task: PublishTestResults@2 + displayName: "Publish test results" + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '*.xml' + searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results" + testRunTitle: "$(Agent.JobName)" + - job: displayName: E2E Tests (SQL Server) variables: @@ -788,13 +799,24 @@ stages: displayName: Copy Playwright results condition: succeededOrFailed() - # Publish + # Publish test artifacts - task: PublishPipelineArtifact@1 displayName: Publish test artifacts condition: succeededOrFailed() inputs: targetPath: $(Build.ArtifactStagingDirectory) artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)" + + # Publish test results + - task: PublishTestResults@2 + displayName: "Publish test results" + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '*.xml' + searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results" + testRunTitle: "$(Agent.JobName)" + ############################################### ## Release ############################################### diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml index 9435aadfd8..8cbb065cd7 100644 --- a/build/nightly-E2E-test-pipelines.yml +++ b/build/nightly-E2E-test-pipelines.yml @@ -248,6 +248,16 @@ stages: targetPath: $(Build.ArtifactStagingDirectory) artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)" + # Publish test results + - task: PublishTestResults@2 + displayName: "Publish test results" + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '*.xml' + searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results" + testRunTitle: "$(Agent.JobName)" + - job: displayName: E2E Tests (SQL Server) timeoutInMinutes: 180 @@ -418,3 +428,13 @@ stages: inputs: targetPath: $(Build.ArtifactStagingDirectory) artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)" + + # Publish test results + - task: PublishTestResults@2 + displayName: "Publish test results" + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '*.xml' + searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results" + testRunTitle: "$(Agent.JobName)" diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/AllowedChildrenDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/AllowedChildrenDocumentTypeController.cs index 5bff749e8a..f7403cb463 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/AllowedChildrenDocumentTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/AllowedChildrenDocumentTypeController.cs @@ -1,5 +1,4 @@ -using Asp.Versioning; -using Microsoft.AspNetCore.Authorization; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; @@ -9,7 +8,6 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; @@ -25,6 +23,15 @@ public class AllowedChildrenDocumentTypeController : DocumentTypeControllerBase _umbracoMapper = umbracoMapper; } + [NonAction] + [Obsolete("Use the non obsoleted method instead. Scheduled to be removed in v16")] + public async Task AllowedChildrenByKey( + CancellationToken cancellationToken, + Guid id, + int skip = 0, + int take = 100) + => await AllowedChildrenByKey(cancellationToken, id, null, skip, take); + [HttpGet("{id:guid}/allowed-children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] @@ -32,10 +39,11 @@ public class AllowedChildrenDocumentTypeController : DocumentTypeControllerBase public async Task AllowedChildrenByKey( CancellationToken cancellationToken, Guid id, + Guid? parentContentKey = null, int skip = 0, int take = 100) { - Attempt?, ContentTypeOperationStatus> attempt = await _contentTypeService.GetAllowedChildrenAsync(id, skip, take); + Attempt?, ContentTypeOperationStatus> attempt = await _contentTypeService.GetAllowedChildrenAsync(id, parentContentKey, skip, take); if (attempt.Success is false) { return OperationStatusResult(attempt.Status); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/AllowedChildrenMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/AllowedChildrenMediaTypeController.cs index 231db5646e..71ee63d6e3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/AllowedChildrenMediaTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/AllowedChildrenMediaTypeController.cs @@ -1,4 +1,4 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; @@ -23,6 +23,15 @@ public class AllowedChildrenMediaTypeController : MediaTypeControllerBase _umbracoMapper = umbracoMapper; } + [NonAction] + [Obsolete("Use the non obsoleted method instead. Scheduled for removal in Umbraco 16.")] + public async Task AllowedChildrenByKey( + CancellationToken cancellationToken, + Guid id, + int skip = 0, + int take = 100) + => await AllowedChildrenByKey(cancellationToken, id, null, skip, take); + [HttpGet("{id:guid}/allowed-children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] @@ -30,10 +39,11 @@ public class AllowedChildrenMediaTypeController : MediaTypeControllerBase public async Task AllowedChildrenByKey( CancellationToken cancellationToken, Guid id, + Guid? parentContentKey = null, int skip = 0, int take = 100) { - Attempt?, ContentTypeOperationStatus> attempt = await _mediaTypeService.GetAllowedChildrenAsync(id, skip, take); + Attempt?, ContentTypeOperationStatus> attempt = await _mediaTypeService.GetAllowedChildrenAsync(id, parentContentKey, skip, take); if (attempt.Success is false) { return OperationStatusResult(attempt.Status); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs index 8f29b922f3..5251ac6f51 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs @@ -17,7 +17,20 @@ public class RebuildPublishedCacheController : PublishedCacheControllerBase [ProducesResponseType(StatusCodes.Status200OK)] public Task Rebuild(CancellationToken cancellationToken) { - _databaseCacheRebuilder.Rebuild(); - return Task.FromResult(Ok()); + if (_databaseCacheRebuilder.IsRebuilding()) + { + var problemDetails = new ProblemDetails + { + Title = "Database cache can not be rebuilt", + Detail = $"The database cache is in the process of rebuilding.", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + + return await Task.FromResult(Conflict(problemDetails)); + } + + _databaseCacheRebuilder.Rebuild(true); + return await Task.FromResult(Ok()); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheStatusController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheStatusController.cs new file mode 100644 index 0000000000..5ecceecd3d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheStatusController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.PublishedCache; +using Umbraco.Cms.Core.PublishedCache; + +namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache; + +[ApiVersion("1.0")] +public class RebuildPublishedCacheStatusController : PublishedCacheControllerBase +{ + private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder; + + public RebuildPublishedCacheStatusController(IDatabaseCacheRebuilder databaseCacheRebuilder) => _databaseCacheRebuilder = databaseCacheRebuilder; + + [HttpGet("rebuild/status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(RebuildStatusModel), StatusCodes.Status200OK)] + public Task Status(CancellationToken cancellationToken) + { + var isRebuilding = _databaseCacheRebuilder.IsRebuilding(); + return Task.FromResult((IActionResult)Ok(new RebuildStatusModel + { + IsRebuilding = isRebuilding + })); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs index 11c9a36e25..3dc491b714 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs @@ -19,6 +19,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Security; public class BackOfficeGraphicsController : Controller { public const string LogoRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(Logo); + public const string LogoAlternativeRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LogoAlternative); public const string LoginBackGroundRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginBackground); public const string LoginLogoRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginLogo); public const string LoginLogoAlternativeRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginLogoAlternative); @@ -44,6 +45,11 @@ public class BackOfficeGraphicsController : Controller [MapToApiVersion("1.0")] public IActionResult Logo() => HandleFileRequest(_contentSettings.Value.BackOfficeLogo); + [HttpGet("logo-alternative", Name = LogoAlternativeRouteName)] + [AllowAnonymous] + [MapToApiVersion("1.0")] + public IActionResult LogoAlternative() => HandleFileRequest(_contentSettings.Value.BackOfficeLogoAlternative); + [HttpGet("login-logo", Name = LoginLogoRouteName)] [AllowAnonymous] [MapToApiVersion("1.0")] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs index 2b3656ad25..07063b69f7 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs @@ -28,6 +28,7 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase private static readonly string _indent = $"{Environment.NewLine} "; + [ActivatorUtilitiesConstructor] public ExecuteTemplateQueryController( IPublishedContentQuery publishedContentQuery, IPublishedValueFallback publishedValueFallback, diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index 4ef42524c4..48e53c97e0 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -64,7 +64,6 @@ public static partial class UmbracoBuilderExtensions .AddWebhooks() .AddServer() .AddCorsPolicy() - .AddWebhooks() .AddPreview() .AddServerEvents() .AddPasswordConfiguration() diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 11b28ec9fd..73c031040e 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -182,7 +182,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -254,7 +254,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -351,7 +351,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -491,7 +491,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -622,7 +622,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -690,7 +690,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -794,7 +794,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -869,7 +869,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -904,7 +904,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -1038,7 +1038,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -1110,7 +1110,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -1207,7 +1207,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -1347,7 +1347,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -1436,7 +1436,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -1592,7 +1592,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -1663,7 +1663,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -1726,7 +1726,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -1788,7 +1788,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -1946,7 +1946,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -2018,7 +2018,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -2115,7 +2115,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -2255,7 +2255,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -2336,7 +2336,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -2466,7 +2466,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -2612,7 +2612,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -2721,7 +2721,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -2784,7 +2784,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -2839,7 +2839,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -2973,7 +2973,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -3045,7 +3045,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -3142,7 +3142,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -3282,7 +3282,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -3398,7 +3398,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -3544,7 +3544,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -3616,7 +3616,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -3713,7 +3713,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -3853,7 +3853,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -3973,7 +3973,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -4082,7 +4082,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -4153,7 +4153,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -4216,7 +4216,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -4350,7 +4350,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -4422,7 +4422,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -4493,7 +4493,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -4633,7 +4633,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -4671,6 +4671,14 @@ "format": "uuid" } }, + { + "name": "parentContentKey", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, { "name": "skip", "in": "query", @@ -4723,7 +4731,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -4801,7 +4809,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -4878,7 +4886,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -5023,7 +5031,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -5096,7 +5104,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -5226,7 +5234,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -5368,7 +5376,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -5435,7 +5443,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -5516,7 +5524,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -5563,7 +5571,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -5697,7 +5705,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -5769,7 +5777,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -5866,7 +5874,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -6006,7 +6014,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -6152,7 +6160,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -6320,7 +6328,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -6391,7 +6399,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -6454,7 +6462,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -6553,7 +6561,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -6627,7 +6635,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -6733,7 +6741,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -6851,7 +6859,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -6992,7 +7000,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -7126,7 +7134,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -7198,7 +7206,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -7295,7 +7303,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -7435,7 +7443,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -7526,7 +7534,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -7645,7 +7653,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -7717,7 +7725,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -7871,7 +7879,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -7987,7 +7995,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -8098,7 +8106,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -8173,7 +8181,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -8275,7 +8283,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -8406,7 +8414,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -8489,7 +8497,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -8559,7 +8567,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -8661,7 +8669,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -8803,7 +8811,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -8945,7 +8953,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -9017,7 +9025,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -9081,7 +9089,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -9145,7 +9153,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -9275,7 +9283,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -9417,7 +9425,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -9560,7 +9568,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -9639,7 +9647,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -9674,7 +9682,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -9793,7 +9801,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -9857,7 +9865,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -9976,7 +9984,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -10177,7 +10185,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -10288,7 +10296,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -10374,7 +10382,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -10504,7 +10512,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -10579,7 +10587,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -10634,7 +10642,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -10682,7 +10690,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -10753,7 +10761,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -10816,7 +10824,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -10894,7 +10902,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -10940,7 +10948,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -10995,7 +11003,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -11054,7 +11062,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -11137,7 +11145,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -11253,7 +11261,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -11430,7 +11438,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -12195,7 +12203,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -12359,7 +12367,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -12498,7 +12506,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -12565,7 +12573,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -12632,7 +12640,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -12727,7 +12735,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -12812,7 +12820,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -12867,7 +12875,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -12973,7 +12981,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -13044,7 +13052,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -13114,7 +13122,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -13182,7 +13190,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -13220,7 +13228,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -13258,7 +13266,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -13638,7 +13646,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -13710,7 +13718,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -13781,7 +13789,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -13921,7 +13929,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -13959,6 +13967,14 @@ "format": "uuid" } }, + { + "name": "parentContentKey", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, { "name": "skip", "in": "query", @@ -14011,7 +14027,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -14088,7 +14104,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -14233,7 +14249,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -14306,7 +14322,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -14436,7 +14452,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -14578,7 +14594,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -14645,7 +14661,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -14726,7 +14742,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -14773,7 +14789,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -14907,7 +14923,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -14979,7 +14995,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -15076,7 +15092,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -15216,7 +15232,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -15362,7 +15378,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -15422,7 +15438,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -15493,7 +15509,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -15556,7 +15572,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -15677,7 +15693,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -15938,7 +15954,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -16010,7 +16026,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -16107,7 +16123,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -16247,7 +16263,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -16338,7 +16354,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -16442,7 +16458,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -16553,7 +16569,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -16629,7 +16645,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -16693,7 +16709,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -16823,7 +16839,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -16902,7 +16918,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -16937,7 +16953,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -17056,7 +17072,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -17120,7 +17136,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -17239,7 +17255,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -17313,7 +17329,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -17424,7 +17440,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -17510,7 +17526,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -17640,7 +17656,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -17715,7 +17731,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -17770,7 +17786,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -17818,7 +17834,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -17889,7 +17905,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -17952,7 +17968,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -18056,7 +18072,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -18162,7 +18178,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -18223,7 +18239,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -18320,7 +18336,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -18460,7 +18476,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -18527,7 +18543,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -18769,7 +18785,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -18841,7 +18857,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -18912,7 +18928,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -19052,7 +19068,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -19141,7 +19157,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -19255,7 +19271,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -19348,7 +19364,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -19395,7 +19411,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -19450,7 +19466,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -19570,7 +19586,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -19823,7 +19839,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -19895,7 +19911,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -19992,7 +20008,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -20132,7 +20148,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -20274,7 +20290,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -20321,7 +20337,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -20440,7 +20456,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -20514,7 +20530,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -20561,7 +20577,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -20596,7 +20612,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -20709,7 +20725,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -20807,7 +20823,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -20854,7 +20870,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -20909,7 +20925,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -21041,7 +21057,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -21113,7 +21129,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -21184,7 +21200,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -21298,7 +21314,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -21371,7 +21387,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -21426,7 +21442,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -21608,7 +21624,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -21679,7 +21695,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -21775,7 +21791,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -21914,7 +21930,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -22070,7 +22086,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -22216,7 +22232,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -22287,7 +22303,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -22383,7 +22399,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -22450,7 +22466,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -22509,7 +22525,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -22556,7 +22572,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -22618,7 +22634,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -22673,7 +22689,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -22764,7 +22780,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -22829,7 +22845,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -22903,7 +22919,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -22980,6 +22996,38 @@ ] } }, + "/umbraco/management/api/v1/published-cache/rebuild/status": { + "get": { + "tags": [ + "Published Cache" + ], + "operationId": "GetPublishedCacheRebuildStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/RebuildStatusModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/published-cache/reload": { "post": { "tags": [ @@ -23101,7 +23149,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -23165,7 +23213,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -23210,7 +23258,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -23257,7 +23305,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -23300,7 +23348,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -23416,7 +23464,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -23476,7 +23524,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -23554,7 +23602,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -23736,7 +23784,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -23807,7 +23855,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -23903,7 +23951,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -24042,7 +24090,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -24198,7 +24246,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -24344,7 +24392,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -24415,7 +24463,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -24511,7 +24559,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -24570,7 +24618,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -24632,7 +24680,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -24687,7 +24735,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -24855,7 +24903,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -24948,7 +24996,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -25079,7 +25127,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -25279,7 +25327,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -25440,7 +25488,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -25825,7 +25873,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -25896,7 +25944,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -25992,7 +26040,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -26131,7 +26179,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -26287,7 +26335,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -26433,7 +26481,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -26504,7 +26552,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -26600,7 +26648,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -26659,7 +26707,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -26721,7 +26769,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -26776,7 +26824,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -26904,7 +26952,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -26939,7 +26987,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -27030,7 +27078,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -27284,7 +27332,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -27356,7 +27404,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -27453,7 +27501,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -27593,7 +27641,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -27640,7 +27688,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -27718,7 +27766,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -27765,7 +27813,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -27813,7 +27861,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -27876,7 +27924,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -27931,7 +27979,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -28321,7 +28369,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -28382,7 +28430,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -28818,7 +28866,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -28960,7 +29008,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -29078,7 +29126,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -29143,7 +29191,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -29203,7 +29251,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -29274,7 +29322,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -29388,7 +29436,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -29513,7 +29561,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -29636,7 +29684,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -29776,7 +29824,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -29959,7 +30007,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -30062,7 +30110,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -30141,7 +30189,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -30201,7 +30249,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -30298,7 +30346,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -30438,7 +30486,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -30513,7 +30561,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -30620,7 +30668,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -30692,7 +30740,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -30822,7 +30870,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -30938,7 +30986,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -30993,7 +31041,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -31074,7 +31122,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -31196,7 +31244,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -31307,7 +31355,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -31447,7 +31495,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -31494,7 +31542,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -32081,7 +32129,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -32418,7 +32466,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -32549,7 +32597,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -32695,7 +32743,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -32823,7 +32871,7 @@ } }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -32949,7 +32997,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -33088,7 +33136,7 @@ } }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -33162,7 +33210,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -33267,7 +33315,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -33383,7 +33431,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -33515,7 +33563,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -33587,7 +33635,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -33684,7 +33732,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -33824,7 +33872,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource", + "description": "The authenticated user does not have access to this resource", "headers": { "Umb-Notifications": { "description": "The list of notifications produced during the request.", @@ -33952,7 +34000,7 @@ "description": "The resource is protected and requires an authentication token" }, "403": { - "description": "The authenticated user do not have access to this resource" + "description": "The authenticated user does not have access to this resource" } }, "security": [ @@ -36826,6 +36874,8 @@ "required": [ "documentType", "id", + "isProtected", + "isTrashed", "sortOrder", "values", "variants" @@ -36871,6 +36921,12 @@ } ] }, + "isTrashed": { + "type": "boolean" + }, + "isProtected": { + "type": "boolean" + }, "updater": { "type": "string", "nullable": true @@ -42881,6 +42937,18 @@ }, "additionalProperties": false }, + "RebuildStatusModel": { + "required": [ + "isRebuilding" + ], + "type": "object", + "properties": { + "isRebuilding": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "RedirectStatusModel": { "enum": [ "Enabled", diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilterBase.cs b/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilterBase.cs index 6557834947..907b91cdac 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilterBase.cs +++ b/src/Umbraco.Cms.Api.Management/OpenApi/BackOfficeSecurityRequirementsOperationFilterBase.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -59,7 +59,7 @@ public abstract class BackOfficeSecurityRequirementsOperationFilterBase : IOpera { operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse() { - Description = "The authenticated user do not have access to this resource" + Description = "The authenticated user does not have access to this resource" }); } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/PublishedCache/RebuildStatusModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/PublishedCache/RebuildStatusModel.cs new file mode 100644 index 0000000000..b966cf4edb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/PublishedCache/RebuildStatusModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.PublishedCache; + +public class RebuildStatusModel +{ + public bool IsRebuilding { get; set; } +} diff --git a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj index 16bac191d5..7563b27d74 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj @@ -4,7 +4,7 @@ Adds imaging support using ImageSharp/ImageSharp.Web version 2 to Umbraco CMS. - + diff --git a/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_blue.svg b/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_blue.svg new file mode 100644 index 0000000000..213e629228 --- /dev/null +++ b/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_dark.svg b/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_dark.svg index 578bf592f6..5a06848ca2 100644 --- a/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_dark.svg +++ b/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_dark.svg @@ -1,51 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_light.svg b/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_light.svg index 01f7260cd3..2cf6f016b5 100644 --- a/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_light.svg +++ b/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_light.svg @@ -1,51 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index 4eecac3fd7..3e89369f00 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -23,6 +23,8 @@ public class ContentSettings internal const string StaticLoginLogoImage = "assets/logo_light.svg"; internal const string StaticLoginLogoImageAlternative = "assets/logo_dark.svg"; internal const string StaticBackOfficeLogo = "assets/logo.svg"; + internal const string StaticBackOfficeLogoAlternative = "assets/logo_blue.svg"; + internal const bool StaticHideBackOfficeLogo = false; internal const bool StaticDisableDeleteWhenReferenced = false; internal const bool StaticDisableUnpublishWhenReferenced = false; internal const bool StaticAllowEditInvariantFromNonDefault = false; @@ -87,9 +89,25 @@ public class ContentSettings /// /// Gets or sets a value for the path to the backoffice logo. /// + /// The alternative version of this logo can be found at . [DefaultValue(StaticBackOfficeLogo)] public string BackOfficeLogo { get; set; } = StaticBackOfficeLogo; + /// + /// Gets or sets a value for the path to the alternative backoffice logo, which can be shown + /// on top of a light background. + /// + /// This is the alternative version to the regular logo found at . + [DefaultValue(StaticBackOfficeLogoAlternative)] + public string BackOfficeLogoAlternative { get; set; } = StaticBackOfficeLogoAlternative; + + /// + /// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not. + /// + [DefaultValue(StaticHideBackOfficeLogo)] + [Obsolete("This setting is no longer used and will be removed in future versions. An alternative BackOffice logo can be set using the BackOfficeLogo setting.")] + public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo; + /// /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items. /// diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 1fb0445bb1..03a4ab185e 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -114,13 +114,23 @@ Mange hilsner fra Umbraco robotten %1% for mange.]]> Ét eller flere områder lever ikke op til kravene for antal indholdselementer. Den valgte medie type er ugyldig. + Det valgte indhold er af en ugyldig type. + Det valgte indhold eksistere ikke. Det er kun tilladt at vælge ét medie. - Valgt medie kommer fra en ugyldig mappe. + Valgt indhold kommer fra en ugyldig mappe. Værdien %0% er mindre end det tilladte minimum af %1%. Værdien %0% er større end det tilladte maksimum af %1%. + Den ene enhed givet er mindre end det tilladte minimum af %1%. + De %0% enheder givet er mindre end det tilladte minimum af %1%. + De %0% enheder givet er større end det tilladte minimum af %1%. Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%. Værdien %0% forventes ikke at indeholde et spænd. + Tekststrengen er længere end den tilladte længde på %0% tegn. Værdien %0% forventes at have en værdi der er større end fra værdien. + Det valgte indhold er af den forkerte type. + "Værdien '%0%' er ikke en af de tilgængelige valgmuligheder. + Værdierne '%0%' er ikke tilstede i de tilgængelige valgmuligheder. + "Den valgte farve '%0%' er ikke en af de tilgængelige valgmuligheder. Slettet indhold med Id: {0} Relateret til original "parent" med id: {1} diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index e1fdba85d4..730fa1a6e9 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -378,6 +378,7 @@ Value is invalid, it does not match the correct pattern %1% more.]]> %1% too many.]]> + The string length exceeds the maximum length of %0% characters. The content amount requirements are not met for one or more areas. Invalid member group name Invalid user group name @@ -389,12 +390,22 @@ Username '%0%' is already taken The value %0% is less than the allowed minimum value of %1% The value %0% is greater than the allowed maximum value of %1% + The 1 item provided is less than the allowed minimum of %1% + The %0% items provided are less than the allowed minimum of %1% + The %0% items provided are greater than the allowed maximum of %1% The value %0% does not correspond with the configured step value of %1% and minimum value of %2% The value %0% is not expected to contain a range The value %0% is not expected to have a to value less than the from value The chosen media type is invalid. + The chosen content is of invalid type. + The chosen content does not exist. Multiple selected media is not allowed. - The selected media is from the wrong folder. + The value '%0%' is not one of the available options. + The values '%0%' are not found in the the available options. + "The selected colour '%0%' is not one of the available options. + The selected item is from the wrong folder. + The selected item is of the wrong type. + "The value '%0%' is not one of the available options. - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/assets/logo_light.svg b/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/assets/logo_light.svg index 01f7260cd3..2cf6f016b5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/assets/logo_light.svg +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/assets/logo_light.svg @@ -1,51 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/backoffice.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/backoffice.handlers.ts index ddf3473bbf..b1760e7bec 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/backoffice.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/backoffice.handlers.ts @@ -2,6 +2,7 @@ const { rest } = window.MockServiceWorker; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; const logoUrl = './src/mocks/handlers/backoffice/assets/logo.svg'; +const logoAlternativeUrl = './src/mocks/handlers/backoffice/assets/logo_blue.svg'; const loginLogoUrl = './src/mocks/handlers/backoffice/assets/logo_light.svg'; const loginLogoAlternativeUrl = './src/mocks/handlers/backoffice/assets/logo_dark.svg'; const loginBackgroundUrl = './src/mocks/handlers/backoffice/assets/login.jpg'; @@ -16,6 +17,15 @@ export const handlers = [ ctx.body(imageBuffer), ); }), + rest.get(umbracoPath('/security/back-office/graphics/logo-alternative'), async (req, res, ctx) => { + const imageBuffer = await fetch(logoAlternativeUrl).then((res) => res.arrayBuffer()); + + return res( + ctx.set('Content-Length', imageBuffer.byteLength.toString()), + ctx.set('Content-Type', 'image/svg+xml'), + ctx.body(imageBuffer), + ); + }), rest.get(umbracoPath('/security/back-office/graphics/login-logo'), async (req, res, ctx) => { const imageBuffer = await fetch(loginLogoUrl).then((res) => res.arrayBuffer()); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.test.ts index 61445c9ab8..571720619f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.test.ts @@ -5,6 +5,7 @@ import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../property-e import type { UmbBlockGridValueModel } from '../../../types.js'; import { UmbBlockToBlockGridClipboardPastePropertyValueTranslator } from './block-to-block-grid-paste-translator.js'; import type { UmbBlockClipboardEntryValueModel } from '@umbraco-cms/backoffice/block'; +import type { UmbBlockGridPropertyEditorConfig } from '../../../property-editors/block-grid-editor/types.js'; @customElement('test-controller-host') class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} @@ -55,22 +56,26 @@ describe('UmbBlockToBlockGridClipboardPastePropertyValueTranslator', () => { settingsData: blockGridPropertyValue.settingsData, }; - const config1: Array<{ alias: string; value: [{ contentElementTypeKey: string }] }> = [ + const config1: UmbBlockGridPropertyEditorConfig = [ { alias: 'blocks', value: [ { + allowAtRoot: true, + allowInAreas: true, contentElementTypeKey: 'contentTypeKey', }, ], }, ]; - const config2: Array<{ alias: string; value: [{ contentElementTypeKey: string }] }> = [ + const config2: UmbBlockGridPropertyEditorConfig = [ { alias: 'blocks', value: [ { + allowAtRoot: true, + allowInAreas: true, contentElementTypeKey: 'contentTypeKey2', }, ], @@ -101,12 +106,12 @@ describe('UmbBlockToBlockGridClipboardPastePropertyValueTranslator', () => { describe('isCompatibleValue', () => { it('returns true if the value is compatible', async () => { - const result = await copyTranslator.isCompatibleValue(blockClipboardEntryValue, config1); + const result = await copyTranslator.isCompatibleValue(blockGridPropertyValue, config1); expect(result).to.be.true; }); it('returns false if the value is not compatible', async () => { - const result = await copyTranslator.isCompatibleValue(blockClipboardEntryValue, config2); + const result = await copyTranslator.isCompatibleValue(blockGridPropertyValue, config2); expect(result).to.be.false; }); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.ts index 1f7cc20ba8..67081389ce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.ts @@ -1,5 +1,6 @@ import type { UmbBlockGridLayoutModel, UmbBlockGridValueModel } from '../../../types.js'; import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../constants.js'; +import type { UmbBlockGridPropertyEditorConfig } from '../../../property-editors/block-grid-editor/types.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbClipboardPastePropertyValueTranslator } from '@umbraco-cms/backoffice/clipboard'; import type { UmbBlockClipboardEntryValueModel, UmbBlockLayoutBaseModel } from '@umbraco-cms/backoffice/block'; @@ -44,19 +45,18 @@ export class UmbBlockToBlockGridClipboardPastePropertyValueTranslator /** * Determines if a block clipboard entry value is compatible with the Block Grid property editor. - * @param {UmbBlockClipboardEntryValueModel} value The block clipboard entry value. - * @param {*} config The Block Grid property editor configuration. + * @param {UmbBlockClipboardEntryValueModel} propertyValue The block clipboard entry value. + * @param {UmbBlockGridPropertyEditorConfig} config The Block Grid property editor configuration. * @returns {Promise} A promise that resolves with a boolean indicating if the value is compatible. * @memberof UmbBlockToBlockGridClipboardPastePropertyValueTranslator */ async isCompatibleValue( - value: UmbBlockClipboardEntryValueModel, - // TODO: Replace any with the correct type. - config: Array<{ alias: string; value: [{ contentElementTypeKey: string }] }>, + propertyValue: UmbBlockGridValueModel, + config: UmbBlockGridPropertyEditorConfig, ): Promise { const allowedBlockContentTypes = config.find((c) => c.alias === 'blocks')?.value.map((b) => b.contentElementTypeKey) ?? []; - const blockContentTypes = value.contentData.map((c) => c.contentTypeKey); + const blockContentTypes = propertyValue.contentData.map((c) => c.contentTypeKey); return blockContentTypes?.every((b) => allowedBlockContentTypes.includes(b)) ?? false; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/paste/grid-block-to-block-grid-paste-translator.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/paste/grid-block-to-block-grid-paste-translator.ts index 150d714843..2b3a2a0e08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/paste/grid-block-to-block-grid-paste-translator.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/paste/grid-block-to-block-grid-paste-translator.ts @@ -1,3 +1,4 @@ +import type { UmbBlockGridPropertyEditorConfig } from '../../../property-editors/block-grid-editor/types.js'; import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../property-editors/constants.js'; import type { UmbBlockGridValueModel } from '../../../types.js'; import type { UmbGridBlockClipboardEntryValueModel } from '../../types.js'; @@ -35,20 +36,22 @@ export class UmbGridBlockToBlockGridClipboardPastePropertyValueTranslator /** * Checks if the clipboard entry value is compatible with the config. - * @param {UmbGridBlockClipboardEntryValueModel} value - The grid block clipboard entry value. + * @param {UmbGridBlockClipboardEntryValueModel} propertyValue - The grid block clipboard entry value. * @param {*} config - The Property Editor config. + * @param {(value, config) => Promise} filter - The filter function. * @returns {Promise} {Promise} * @memberof UmbGridBlockToBlockGridClipboardPastePropertyValueTranslator */ async isCompatibleValue( - value: UmbGridBlockClipboardEntryValueModel, - // TODO: Replace any with the correct type. - config: Array<{ alias: string; value: [{ contentElementTypeKey: string }] }>, + propertyValue: UmbBlockGridValueModel, + config: UmbBlockGridPropertyEditorConfig, + filter?: (propertyValue: UmbBlockGridValueModel, config: UmbBlockGridPropertyEditorConfig) => Promise, ): Promise { - const allowedBlockContentTypes = - config.find((c) => c.alias === 'blocks')?.value.map((b) => b.contentElementTypeKey) ?? []; - const blockContentTypes = value.contentData.map((c) => c.contentTypeKey); - return blockContentTypes?.every((b) => allowedBlockContentTypes.includes(b)) ?? false; + const blocksConfig = config.find((c) => c.alias === 'blocks'); + const allowedBlockContentTypes = blocksConfig?.value.map((b) => b.contentElementTypeKey) ?? []; + const blockContentTypes = propertyValue.contentData.map((c) => c.contentTypeKey); + const allContentTypesAllowed = blockContentTypes?.every((b) => allowedBlockContentTypes.includes(b)) ?? false; + return allContentTypesAllowed && (!filter || (await filter(propertyValue, config))); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/manifests.ts index 6546d9da11..7fdabef375 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/manifests.ts @@ -1,6 +1,7 @@ import { UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS } from '../property-editors/constants.js'; import { manifests as blockManifests } from './block/manifests.js'; import { manifests as gridBlockManifests } from './grid-block/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_PROPERTY_HAS_VALUE_CONDITION_ALIAS, UMB_WRITABLE_PROPERTY_CONDITION_ALIAS, @@ -8,7 +9,7 @@ import { const forPropertyEditorUis = [UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS]; -export const manifests: Array = [ +export const manifests: Array = [ { type: 'propertyContext', kind: 'clipboard', @@ -31,18 +32,6 @@ export const manifests: Array = [ }, ], }, - { - type: 'propertyAction', - kind: 'pasteFromClipboard', - alias: 'Umb.PropertyAction.BlockGrid.Clipboard.Paste', - name: 'Block Grid Paste From Clipboard Property Action', - forPropertyEditorUis, - conditions: [ - { - alias: UMB_WRITABLE_PROPERTY_CONDITION_ALIAS, - }, - ], - }, ...blockManifests, ...gridBlockManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts index 008acd78ca..80807a87b1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts @@ -446,7 +446,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper }; override render() { - return this.contentKey + return this.contentKey && (this._contentTypeAlias || this._unsupported) ? html` ${this.#renderCreateBeforeInlineButton()}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts index bb1fe4cc37..e410f894d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts @@ -14,6 +14,7 @@ import type { UmbBlockGridValueModel, } from '../types.js'; import { forEachBlockLayoutEntryOf } from '../utils/index.js'; +import type { UmbBlockGridPropertyEditorConfig } from '../property-editors/block-grid-editor/types.js'; import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js'; import type { UmbBlockGridScalableContainerContext } from './block-grid-scale-manager/block-grid-scale-manager.controller.js'; import { @@ -170,7 +171,7 @@ export class UmbBlockGridEntriesContext // TODO: consider moving some of this logic to the clipboard property context const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT); - const config = propertyContext.getConfig(); + const config = propertyContext.getConfig() as UmbBlockGridPropertyEditorConfig; const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this); return { @@ -198,7 +199,8 @@ export class UmbBlockGridEntriesContext clipboardEntryDetail.values, UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS, ); - return pasteTranslator.isCompatibleValue(value, config); + + return pasteTranslator.isCompatibleValue(value, config, (value) => this.#clipboardEntriesFilter(value)); } return true; @@ -269,6 +271,21 @@ export class UmbBlockGridEntriesContext }); } + async #clipboardEntriesFilter(propertyValue: UmbBlockGridValueModel) { + const allowedElementTypeKeys = this.#retrieveAllowedElementTypes().map((x) => x.contentElementTypeKey); + + const rootContentKeys = propertyValue.layout['Umbraco.BlockGrid']?.map((block) => block.contentKey) ?? []; + const rootContentTypeKeys = propertyValue.contentData + .filter((content) => rootContentKeys.includes(content.key)) + .map((content) => content.contentTypeKey); + + const allContentTypesAllowed = rootContentTypeKeys.every((contentKey) => + allowedElementTypeKeys.includes(contentKey), + ); + + return allContentTypesAllowed; + } + protected _gotBlockManager() { if (!this._manager) return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/manifests.ts index 209e3eef67..5b760bd190 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/manifests.ts @@ -3,8 +3,9 @@ import { manifests as componentManifests } from './components/manifests.js'; import { manifests as propertyEditorManifests } from './property-editors/manifests.js'; import { manifests as propertyValueClonerManifests } from './property-value-cloner/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [ +export const manifests: Array = [ ...clipboardManifests, ...componentManifests, ...propertyEditorManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/manifests.ts index c6cc6a7dcd..c575670a46 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/manifests.ts @@ -1,8 +1,10 @@ import { manifest as blockGridSchemaManifest } from './Umbraco.BlockGrid.js'; import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS } from './constants.js'; +import { manifests as propertyActionManifests } from './property-actions/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import { UmbStandardBlockValueResolver } from '@umbraco-cms/backoffice/block'; -export const manifests: Array = [ +export const manifests: Array = [ { type: 'propertyEditorUi', alias: UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS, @@ -66,7 +68,6 @@ export const manifests: Array = [ }, }, }, - blockGridSchemaManifest, { type: 'propertyValueResolver', alias: 'Umb.PropertyValueResolver.BlockGrid', @@ -76,4 +77,6 @@ export const manifests: Array = [ editorAlias: UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, }, }, + blockGridSchemaManifest, + ...propertyActionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-actions/block-grid-paste-from-clipboard.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-actions/block-grid-paste-from-clipboard.ts new file mode 100644 index 0000000000..1480e09dcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-actions/block-grid-paste-from-clipboard.ts @@ -0,0 +1,52 @@ +import type { UmbBlockGridValueModel } from '../../../types.js'; +import type { UmbBlockGridPropertyEditorConfig } from '../types.js'; +import { UmbPasteFromClipboardPropertyAction } from '@umbraco-cms/backoffice/clipboard'; + +/** + * The Block Grid Paste From Clipboard Property Action. + * @exports + * @class UmbBlockGridPasteFromClipboardPropertyAction + * @augments UmbPasteFromClipboardPropertyAction + */ +export class UmbBlockGridPasteFromClipboardPropertyAction extends UmbPasteFromClipboardPropertyAction { + /** + * Filters the picker based on the block grid property editor config. + * @param {UmbBlockGridValueModel} propertyValue The property editor value. + * @param {UmbBlockGridPropertyEditorConfig} config The property editor config. + * @override + * @protected + * @memberof UmbBlockGridPasteFromClipboardPropertyAction + */ + protected override async _pickerFilter( + propertyValue: UmbBlockGridValueModel, + config: UmbBlockGridPropertyEditorConfig, + ) { + // The property action always paste in the root of the grid so + // we need to check if the content types are allowed at the root + const blocksConfig = config.find((configValue) => configValue.alias === 'blocks'); + + const allowedRootContentTypeKeys = + blocksConfig?.value + .map((blockConfig) => { + if (blockConfig.allowAtRoot) { + return blockConfig.contentElementTypeKey; + } else { + return undefined; + } + }) + .filter((contentTypeKey) => contentTypeKey !== undefined) ?? []; + + const rootContentKeys = propertyValue.layout['Umbraco.BlockGrid']?.map((block) => block.contentKey) ?? []; + const rootContentTypeKeys = propertyValue.contentData + .filter((content) => rootContentKeys.includes(content.key)) + .map((content) => content.contentTypeKey); + + // ensure all content types in the paste value are allowed in the grid root + const allContentTypesAllowedAtRoot = rootContentTypeKeys.every((contentKey) => + allowedRootContentTypeKeys.includes(contentKey), + ); + + return allContentTypesAllowedAtRoot; + } +} +export { UmbBlockGridPasteFromClipboardPropertyAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-actions/manifests.ts new file mode 100644 index 0000000000..8b2befb3a1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-actions/manifests.ts @@ -0,0 +1,20 @@ +import { UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS } from '../constants.js'; +import { UMB_WRITABLE_PROPERTY_CONDITION_ALIAS } from '@umbraco-cms/backoffice/property'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_PROPERTY_ACTION_PASTE_FROM_CLIPBOARD_KIND_MANIFEST } from '@umbraco-cms/backoffice/clipboard'; + +export const manifests: Array = [ + { + ...UMB_PROPERTY_ACTION_PASTE_FROM_CLIPBOARD_KIND_MANIFEST.manifest, + type: 'propertyAction', + alias: 'Umb.PropertyAction.BlockGrid.Clipboard.Paste', + name: 'Block Grid Paste From Clipboard Property Action', + api: () => import('./block-grid-paste-from-clipboard.js'), + forPropertyEditorUis: [UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS], + conditions: [ + { + alias: UMB_WRITABLE_PROPERTY_CONDITION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/types.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/types.ts new file mode 100644 index 0000000000..99e701404c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/types.ts @@ -0,0 +1,9 @@ +// TODO: add the missing fields to the type +export type UmbBlockGridPropertyEditorConfig = Array<{ + alias: 'blocks'; + value: Array<{ + allowAtRoot: boolean; + allowInAreas: boolean; + contentElementTypeKey: string; + }>; +}>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/manifests.ts index 8a797e8068..37621422be 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/manifests.ts @@ -5,8 +5,9 @@ import { manifests as blockGridEditorManifests } from './block-grid-editor/manif import { manifest as blockGridGroupConfiguration } from './block-grid-group-configuration/manifests.js'; import { manifest as blockGridLayoutStylesheet } from './block-grid-layout-stylesheet/manifests.js'; import { manifest as blockGridTypeConfiguration } from './block-grid-type-configuration/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [ +export const manifests: Array = [ blockGridAreaTypePermission, blockGridAreasConfigEditor, blockGridColumnSpan, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.test.ts index a54f811c9c..56fb74df42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.test.ts @@ -98,12 +98,12 @@ describe('UmbBlockToBlockListClipboardPastePropertyValueTranslator', () => { describe('isCompatibleValue', () => { it('should return true if the content types are allowed', async () => { - const result = await pasteTranslator.isCompatibleValue(blockClipboardEntryValue, config); + const result = await pasteTranslator.isCompatibleValue(blockListPropertyValue, config); expect(result).to.be.true; }); it('should return false if the content types are not allowed', async () => { - const result = await pasteTranslator.isCompatibleValue(blockClipboardEntryValue, config2); + const result = await pasteTranslator.isCompatibleValue(blockListPropertyValue, config2); expect(result).to.be.false; }); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.ts index 9493c4ef61..32fe61adef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.ts @@ -35,19 +35,19 @@ export class UmbBlockToBlockListClipboardPastePropertyValueTranslator /** * Checks if the clipboard entry value is compatible with the config. - * @param {UmbBlockClipboardEntryValueModel} value - The block clipboard entry value. + * @param {UmbBlockListValueModel} propertyValue - The property value * @param {*} config - The Property Editor config. * @returns {Promise} - Whether the clipboard entry value is compatible with the config. * @memberof UmbBlockToBlockListClipboardPastePropertyValueTranslator */ async isCompatibleValue( - value: UmbBlockClipboardEntryValueModel, + propertyValue: UmbBlockListValueModel, // TODO: Replace any with the correct type. config: Array<{ alias: string; value: [{ contentElementTypeKey: string }] }>, ): Promise { const allowedBlockContentTypes = config.find((c) => c.alias === 'blocks')?.value.map((b) => b.contentElementTypeKey) ?? []; - const blockContentTypes = value.contentData.map((c) => c.contentTypeKey); + const blockContentTypes = propertyValue.contentData.map((c) => c.contentTypeKey); return blockContentTypes?.every((b) => allowedBlockContentTypes.includes(b)) ?? false; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts index 48624dad3f..af584330ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts @@ -395,7 +395,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper } #renderBlock() { - return this.contentKey + return this.contentKey && (this._contentTypeAlias || this._unsupported) ? html`
{ - this._contentElementTypeAlias = alias; + this._contentTypeAlias = alias; }, null, ); @@ -230,7 +230,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert } readonly #filterBlockCustomViews = (manifest: ManifestBlockEditorCustomView) => { - const elementTypeAlias = this._contentElementTypeAlias ?? ''; + const elementTypeAlias = this._contentTypeAlias ?? ''; const isForBlockEditor = !manifest.forBlockEditor || stringOrStringArrayContains(manifest.forBlockEditor, UMB_BLOCK_RTE); const isForContentTypeAlias = @@ -256,23 +256,25 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert }; #renderBlock() { - return html` -
- - ${this.#renderRefBlock()} - - ${this.#renderEditAction()} ${this.#renderEditSettingsAction()} - ${!this._showContentEdit && this._contentInvalid - ? html`!` - : nothing} -
- `; + return this.contentKey && this._contentTypeAlias + ? html` +
+ + ${this.#renderRefBlock()} + + ${this.#renderEditAction()} ${this.#renderEditSettingsAction()} + ${!this._showContentEdit && this._contentInvalid + ? html`!` + : nothing} +
+ ` + : nothing; } #renderRefBlock() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/constants.ts index adfc175c42..607c1a68da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/constants.ts @@ -1,4 +1,4 @@ export * from './clipboard-entry/constants.js'; export * from './clipboard-root/constants.js'; export * from './collection/constants.js'; -export * from './property/context/constants.js'; +export * from './property/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/constants.ts new file mode 100644 index 0000000000..93931d5045 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/constants.ts @@ -0,0 +1 @@ +export * from './paste/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/index.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/index.ts new file mode 100644 index 0000000000..50023908eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/index.ts @@ -0,0 +1 @@ +export * from './paste/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/constants.ts new file mode 100644 index 0000000000..b7fe0395d4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/constants.ts @@ -0,0 +1 @@ +export { UMB_PROPERTY_ACTION_PASTE_FROM_CLIPBOARD_KIND_MANIFEST } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/index.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/index.ts new file mode 100644 index 0000000000..8150ab49c4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/index.ts @@ -0,0 +1 @@ +export { UmbPasteFromClipboardPropertyAction } from './paste-from-clipboard.property-action.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/manifests.ts index 3c0ac1d980..44989826c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/manifests.ts @@ -1,22 +1,24 @@ import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_PROPERTY_ACTION_DEFAULT_KIND_MANIFEST } from '@umbraco-cms/backoffice/property-action'; -export const manifests: Array = [ - { - type: 'kind', - alias: 'Umb.Kind.PropertyAction.pasteFromClipboard', - matchKind: 'pasteFromClipboard', - matchType: 'propertyAction', - manifest: { - ...UMB_PROPERTY_ACTION_DEFAULT_KIND_MANIFEST.manifest, - type: 'propertyAction', - kind: 'pasteFromClipboard', - api: () => import('./paste-from-clipboard.property-action.js'), - weight: 1190, - meta: { - icon: 'icon-clipboard-paste', - label: 'Replace', - }, +export const UMB_PROPERTY_ACTION_PASTE_FROM_CLIPBOARD_KIND_MANIFEST: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.PropertyAction.pasteFromClipboard', + matchKind: 'pasteFromClipboard', + matchType: 'propertyAction', + manifest: { + ...UMB_PROPERTY_ACTION_DEFAULT_KIND_MANIFEST.manifest, + type: 'propertyAction', + kind: 'pasteFromClipboard', + api: () => import('./paste-from-clipboard.property-action.js'), + weight: 1190, + meta: { + icon: 'icon-clipboard-paste', + label: 'Replace', }, }, +}; + +export const manifests: Array = [ + UMB_PROPERTY_ACTION_PASTE_FROM_CLIPBOARD_KIND_MANIFEST, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/paste-from-clipboard.property-action.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/paste-from-clipboard.property-action.ts index 869b3ad05b..e4c54117e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/paste-from-clipboard.property-action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/actions/paste/paste-from-clipboard.property-action.ts @@ -25,6 +25,11 @@ export class UmbPasteFromClipboardPropertyAction extends UmbPropertyActionBase Promise} args.filter - A filter function to filter clipboard entries * @returns { Promise<{ selection: Array; propertyValues: Array }> } */ async pick(args: { multiple: boolean; propertyEditorUiAlias: string; + filter?: (value: any, config: any) => Promise; }): Promise<{ selection: Array; propertyValues: Array }> { await this.#init; @@ -149,8 +151,12 @@ export class UmbClipboardPropertyContext extends UmbContextBase extends UmbApi { - translate: (value: ClipboardEntryValueType) => Promise; - isCompatibleValue?: (value: ClipboardEntryValueType, config: ConfigType) => Promise; + translate: (clipboardEntryValue: ClipboardEntryValueType) => Promise; + isCompatibleValue?: ( + propertyValue: PropertyValueType, + config: ConfigType, + filter?: (propertyValue: PropertyValueType, config: ConfigType) => Promise, + ) => Promise; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.element.ts index 8d68d78068..b78188b73b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.element.ts @@ -1,4 +1,4 @@ -import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, css } from '@umbraco-cms/backoffice/external/lit'; import { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; export type InputDateType = 'date' | 'time' | 'datetime-local'; @@ -28,6 +28,16 @@ export class UmbInputDateElement extends UUIInputElement { super(); this.type = 'date'; // Default to 'date' } + + // Adding styles override to add a darkmode version. + static override styles = [ + ...UUIInputElement.styles, + css` + input { + color-scheme: var(--uui-color-scheme, normal); + } + `, + ]; } export default UmbInputDateElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/global-components/content-type-workspace-editor-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/global-components/content-type-workspace-editor-header.element.ts new file mode 100644 index 0000000000..fc3180ee1d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/global-components/content-type-workspace-editor-header.element.ts @@ -0,0 +1,156 @@ +import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '../workspace/content-type-workspace.context-token.js'; +import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components'; +import { umbFocus, UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/icon'; +import type { UUITextareaElement } from '@umbraco-cms/backoffice/external/uui'; +import { umbBindToValidation } from '@umbraco-cms/backoffice/validation'; + +@customElement('umb-content-type-workspace-editor-header') +export class UmbContentTypeWorkspaceEditorHeaderElement extends UmbLitElement { + @state() + private _name?: string; + + @state() + private _alias?: string; + + @state() + private _description?: string; + + @state() + private _icon?: string; + + @state() + private _isNew?: boolean; + + #workspaceContext?: typeof UMB_CONTENT_TYPE_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + this.#observeContentType(); + }); + } + + #observeContentType() { + if (!this.#workspaceContext) return; + this.observe(this.#workspaceContext.name, (name) => (this._name = name), '_observeName'); + this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias), '_observeAlias'); + this.observe( + this.#workspaceContext.description, + (description) => (this._description = description), + '_observeDescription', + ); + this.observe(this.#workspaceContext.icon, (icon) => (this._icon = icon), '_observeIcon'); + this.observe(this.#workspaceContext.isNew, (isNew) => (this._isNew = isNew), '_observeIsNew'); + } + + private async _handleIconClick() { + const [alias, color] = this._icon?.replace('color-', '')?.split(' ') ?? []; + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalContext = modalManager.open(this, UMB_ICON_PICKER_MODAL, { + value: { + icon: alias, + color: color, + }, + }); + + modalContext?.onSubmit().then((saved) => { + if (saved.icon && saved.color) { + this.#workspaceContext?.setIcon(`${saved.icon} color-${saved.color}`); + } else if (saved.icon) { + this.#workspaceContext?.setIcon(saved.icon); + } + }); + } + + #onNameAndAliasChange(event: InputEvent & { target: UmbInputWithAliasElement }) { + this.#workspaceContext?.setName(event.target.value ?? ''); + this.#workspaceContext?.setAlias(event.target.alias ?? ''); + } + + #onDescriptionChange(event: InputEvent & { target: UUITextareaElement }) { + this.#workspaceContext?.setDescription(event.target.value.toString() ?? ''); + } + + override render() { + return html` + + `; + } + + static override styles = [ + css` + :host { + display: contents; + } + + #header { + display: flex; + flex: 1 1 auto; + gap: var(--uui-size-space-2); + } + + #editors { + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: var(--uui-size-space-1); + } + + #name { + width: 100%; + } + + #description { + width: 100%; + --uui-input-height: var(--uui-size-8); + --uui-input-border-color: transparent; + } + + #description:hover { + --uui-input-border-color: var(--uui-color-border); + } + + #icon { + font-size: var(--uui-size-8); + height: 60px; + width: 60px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-content-type-workspace-editor-header': UmbContentTypeWorkspaceEditorHeaderElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/global-components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/global-components/index.ts new file mode 100644 index 0000000000..e9575c6d1b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/global-components/index.ts @@ -0,0 +1,3 @@ +import './content-type-workspace-editor-header.element.js'; + +export * from './content-type-workspace-editor-header.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/index.ts index 3bedcf0cc9..a43707b91e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/index.ts @@ -1,4 +1,5 @@ export * from './constants.js'; +export * from './global-components/index.js'; export * from './repository/index.js'; export * from './structure/index.js'; export * from './workspace/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-data-source.interface.ts index dde79453a2..49d31fe5be 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-data-source.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-data-source.interface.ts @@ -6,5 +6,5 @@ export interface UmbContentTypeStructureDataSourceConstructor { } export interface UmbContentTypeStructureDataSource { - getAllowedChildrenOf(unique: string | null): Promise>>; + getAllowedChildrenOf(unique: string | null, parentContentUnique: string | null): Promise>>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-repository-base.ts index 91cddbe518..4d51925ed8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-repository-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-repository-base.ts @@ -23,7 +23,7 @@ export abstract class UmbContentTypeStructureRepositoryBase * @returns {*} * @memberof UmbContentTypeStructureRepositoryBase */ - requestAllowedChildrenOf(unique: string | null) { - return this.#structureSource.getAllowedChildrenOf(unique); + requestAllowedChildrenOf(unique: string | null, parentContentUnique: string | null) { + return this.#structureSource.getAllowedChildrenOf(unique, parentContentUnique); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-repository.interface.ts index 7691800e44..1f74549895 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-repository.interface.ts @@ -1,5 +1,5 @@ import type { UmbDataSourceResponse, UmbPagedModel } from '@umbraco-cms/backoffice/repository'; export interface UmbContentTypeStructureRepository { - requestAllowedChildrenOf(unique: string): Promise>>; + requestAllowedChildrenOf(unique: string, parentContentUnique: string | null): Promise>>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-server-data-source-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-server-data-source-base.ts index e3294c5283..3ce3d6ae67 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-server-data-source-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/repository/structure/content-type-structure-server-data-source-base.ts @@ -16,7 +16,7 @@ export interface UmbContentTypeStructureServerDataSourceBaseArgs< ServerItemType extends AllowedContentTypeBaseModel, ClientItemType extends UmbEntityModel, > { - getAllowedChildrenOf: (unique: string | null) => Promise>; + getAllowedChildrenOf: (unique: string | null, parentContentUnique: string | null) => Promise>; mapper: (item: ServerItemType) => ClientItemType; } @@ -50,8 +50,8 @@ export abstract class UmbContentTypeStructureServerDataSourceBase< * @returns {*} * @memberof UmbContentTypeStructureServerDataSourceBase */ - async getAllowedChildrenOf(unique: string | null) { - const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAllowedChildrenOf(unique)); + async getAllowedChildrenOf(unique: string | null, parentContentUnique: string | null) { + const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAllowedChildrenOf(unique, parentContentUnique)); if (data) { const items = data.items.map((item) => this.#mapper(item)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts index 74c3f510d1..42fd994550 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts @@ -252,6 +252,14 @@ export abstract class UmbContentTypeWorkspaceContextBase< this.structure.updateOwnerContentType({ compositions } as Partial); } + /** + * Gets the icon of the content type + * @returns { string | undefined } The icon of the content type + */ + public getIcon(): string | undefined { + return this.structure.getOwnerContentType()?.icon; + } + // TODO: manage setting icon color alias? public setIcon(icon: string) { this.structure.updateOwnerContentType({ icon } as Partial); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context.interface.ts index 1405ff101c..dc529f7980 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context.interface.ts @@ -8,7 +8,6 @@ export interface UmbContentTypeWorkspaceContext; - getName(): string | undefined; readonly alias: Observable; readonly description: Observable; readonly icon: Observable; @@ -22,7 +21,18 @@ export interface UmbContentTypeWorkspaceContext; + getAlias(): string | undefined; setAlias(alias: string): void; + getCompositions(): Array | undefined; setCompositions(compositions: Array): void; + + getDescription(): string | undefined; + setDescription(description: string): void; + + getIcon(): string | undefined; + setIcon(icon: string): void; + + getName(): string | undefined; + setName(name: string): void; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/constants.ts index d60c068006..c6a840d160 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/constants.ts @@ -1 +1,2 @@ export * from './common/constants.js'; +export * from './has-children/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/constants.ts new file mode 100644 index 0000000000..cdfbe5b0b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/constants.ts @@ -0,0 +1 @@ +export const UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS = 'Umb.Condition.EntityHasChildren'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition-config.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition-config.ts new file mode 100644 index 0000000000..2c08118a07 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition-config.ts @@ -0,0 +1,12 @@ +import type { UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS } from './constants.js'; +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbEntityHasChildrenConditionConfig + extends UmbConditionConfigBase {} + +declare global { + interface UmbExtensionConditionConfigMap { + UmbEntityHasChildrenConditionConfig: UmbEntityHasChildrenConditionConfig; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition.manifest.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition.manifest.ts new file mode 100644 index 0000000000..ddfde4f2a9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition.manifest.ts @@ -0,0 +1,9 @@ +import { UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS } from './constants.js'; +import type { ManifestCondition } from '@umbraco-cms/backoffice/extension-api'; + +export const manifest: ManifestCondition = { + type: 'condition', + name: 'Entity Has Children Condition', + alias: UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS, + api: () => import('./entity-has-children.condition.js'), +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition.ts new file mode 100644 index 0000000000..cc930c4cf8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/condition/entity-has-children.condition.ts @@ -0,0 +1,25 @@ +import { UMB_HAS_CHILDREN_ENTITY_CONTEXT } from '../context/has-children.context-token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { + UmbConditionConfigBase, + UmbConditionControllerArguments, + UmbExtensionCondition, +} from '@umbraco-cms/backoffice/extension-api'; +import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; + +export class UmbEntityHasChildrenCondition + extends UmbConditionBase + implements UmbExtensionCondition +{ + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); + + this.consumeContext(UMB_HAS_CHILDREN_ENTITY_CONTEXT, (context) => { + this.observe(context.hasChildren, (hasChildren) => { + this.permitted = hasChildren === true; + }); + }); + } +} + +export { UmbEntityHasChildrenCondition as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/constants.ts new file mode 100644 index 0000000000..853290d6b2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/constants.ts @@ -0,0 +1,2 @@ +export * from './condition/constants.js'; +export * from './context/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/constants.ts new file mode 100644 index 0000000000..137e79f843 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/constants.ts @@ -0,0 +1 @@ +export * from './has-children.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/has-children.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/has-children.context-token.ts new file mode 100644 index 0000000000..a2bb77d5e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/has-children.context-token.ts @@ -0,0 +1,6 @@ +import type { UmbHasChildrenEntityContext } from './has-children.entity-context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_HAS_CHILDREN_ENTITY_CONTEXT = new UmbContextToken( + 'UmbHasChildrenEntityContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/has-children.entity-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/has-children.entity-context.ts new file mode 100644 index 0000000000..ed8d1c0de9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/has-children.entity-context.ts @@ -0,0 +1,31 @@ +import { UMB_HAS_CHILDREN_ENTITY_CONTEXT } from './has-children.context-token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbHasChildrenEntityContext extends UmbContextBase { + #hasChildren = new UmbBooleanState(undefined); + public readonly hasChildren = this.#hasChildren.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_HAS_CHILDREN_ENTITY_CONTEXT); + } + + /** + * Gets the hasChildren state + * @returns {boolean} - The hasChildren state + * @memberof UmbHasChildrenEntityContext + */ + public getHasChildren(): boolean | undefined { + return this.#hasChildren.getValue(); + } + + /** + * Sets the hasChildren state + * @param {boolean} hasChildren - The hasChildren state + * @memberof UmbHasChildrenEntityContext + */ + public setHasChildren(hasChildren: boolean) { + this.#hasChildren.setValue(hasChildren); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/index.ts new file mode 100644 index 0000000000..0b119e565c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/context/index.ts @@ -0,0 +1 @@ +export * from './has-children.entity-context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/index.ts new file mode 100644 index 0000000000..00c55032bc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/index.ts @@ -0,0 +1 @@ +export * from './context/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/manifests.ts new file mode 100644 index 0000000000..e3b60432b4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/has-children/manifests.ts @@ -0,0 +1,4 @@ +import { manifest as conditionManifest } from './condition/entity-has-children.condition.manifest.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [conditionManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts index 886f167a42..2104b5fe36 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts @@ -5,8 +5,10 @@ export * from './constants.js'; export * from './entity-action-base.js'; export * from './entity-action-list.element.js'; export * from './entity-action.event.js'; +export * from './has-children/index.js'; export * from './entity-updated.event.js'; export * from './entity-deleted.event.js'; + export type * from './types.js'; export { UmbRequestReloadStructureForEntityEvent } from './request-reload-structure-for-entity.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/manifests.ts index 7d6813e49a..66347b90e6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/manifests.ts @@ -2,6 +2,7 @@ import { manifests as createEntityActionManifests } from './common/create/manife import { manifests as defaultEntityActionManifests } from './default/manifests.js'; import { manifests as deleteEntityActionManifests } from './common/delete/manifests.js'; import { manifests as duplicateEntityActionManifests } from './common/duplicate/manifests.js'; +import { manifests as hasChildrenManifests } from './has-children/manifests.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; @@ -10,4 +11,5 @@ export const manifests: Array = ...defaultEntityActionManifests, ...deleteEntityActionManifests, ...duplicateEntityActionManifests, + ...hasChildrenManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/tree/tree-item/recycle-bin-tree-item.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/tree/tree-item/recycle-bin-tree-item.context.ts index ff1156ed0b..c0091158e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/tree/tree-item/recycle-bin-tree-item.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/tree/tree-item/recycle-bin-tree-item.context.ts @@ -35,7 +35,7 @@ export class UmbRecycleBinTreeItemContext< const supportedEntityTypes = this.getManifest()?.meta.supportedEntityTypes; if (!supportedEntityTypes) { - throw new Error('Supported entity types are missing from the manifest. (manifest.meta.supportedEntityTypes)'); + throw new Error('Entity types are missing from the manifest (manifest.meta.supportedEntityTypes).'); } if (supportedEntityTypes.includes(entityType)) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/delete-folder/delete-folder.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/delete-folder/delete-folder.action.ts index f076800505..4947ccee23 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/delete-folder/delete-folder.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/delete-folder/delete-folder.action.ts @@ -40,7 +40,7 @@ export class UmbDeleteFolderEntityAction extends UmbEntityActionBase 0); + const hasChildren = data.total > 0; + this.#hasChildren.setValue(hasChildren); + this.#hasChildrenContext.setHasChildren(hasChildren); + this.pagination.setTotalItems(data.total); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/workspace-action-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/workspace-action-menu.element.ts index 9f1e456f55..5085ef7a3c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/workspace-action-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/workspace-action-menu.element.ts @@ -27,34 +27,32 @@ export class UmbWorkspaceActionMenuElement extends UmbLitElement { } override render() { - return this.items?.length - ? html` - - - - - - - ${repeat( - this.items, - (ext) => ext.alias, - (ext) => ext.component, - )} - - - - ` - : nothing; + if (!this.items?.length) return nothing; + + return html` + + + + + + ${repeat( + this.items, + (ext) => ext.alias, + (ext) => ext.component, + )} + + + `; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/workspace-action-default-kind.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/workspace-action-default-kind.element.ts index 166a57e22f..54b4a164dd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/workspace-action-default-kind.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/workspace-action-default-kind.element.ts @@ -13,6 +13,7 @@ import { type UmbExtensionElementAndApiInitializer, UmbExtensionsElementAndApiInitializer, } from '@umbraco-cms/backoffice/extension-api'; +import { stringOrStringArrayIntersects } from '@umbraco-cms/backoffice/utils'; import '../../workspace-action-menu/index.js'; @@ -38,7 +39,6 @@ export class UmbWorkspaceActionElement< this._href = value?.meta.href; this._additionalOptions = value?.meta.additionalOptions; this.#createAliases(); - this.requestUpdate('manifest', oldValue); } } public get manifest() { @@ -90,7 +90,10 @@ export class UmbWorkspaceActionElement< // TODO: This works on one level for now, which will be enough for the current use case. However, you can overwrite the overwrites, so we need to make this recursive. Perhaps we could move this to the extensions initializer. // Add overwrites so that we can show any previously registered actions on the original workspace action if (this.#manifest.overwrites) { - for (const alias of this.#manifest.overwrites) { + const overwrites = Array.isArray(this.#manifest.overwrites) + ? this.#manifest.overwrites + : [this.#manifest.overwrites]; + for (const alias of overwrites) { aliases.add(alias); } } @@ -145,11 +148,7 @@ export class UmbWorkspaceActionElement< umbExtensionsRegistry, 'workspaceActionMenuItem', ExtensionApiArgsMethod, - (action) => { - return Array.isArray(action.forWorkspaceActions) - ? action.forWorkspaceActions.some((alias) => aliases.includes(alias)) - : aliases.includes(action.forWorkspaceActions); - }, + (action) => stringOrStringArrayIntersects(action.forWorkspaceActions, aliases), (extensionControllers) => { this._items = extensionControllers; }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts index 33e757f4b9..993986e58e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts @@ -227,14 +227,35 @@ export class UmbDataTypeWorkspaceContext const data = this._data.getCurrent(); if (!data) return; + // We are going to transfer the default data from the schema and the UI (the UI can override the schema data). + // Let us figure out which editors are alike from the inherited data, so we can keep that data around and only transfer the data that is not + // inherited from the previous data type. this.#settingsDefaultData = [ ...this.#propertyEditorSchemaSettingsDefaultData, ...this.#propertyEditorUISettingsDefaultData, ] satisfies Array; - // We check for satisfied type, because we will be directly transferring them to become value. Future note, if they are not satisfied, we need to transfer alias and value. [NL] - this._data.updatePersisted({ values: this.#settingsDefaultData }); - this._data.updateCurrent({ values: this.#settingsDefaultData }); + const values: Array = []; + + // We want to keep the existing data, if it is not in the default data, and if it is in the default data, then we want to keep the default data. + for (const defaultDataItem of this.#properties.getValue()) { + // We are matching on the alias, as we assume that the alias is unique for the data type. + // TODO: Consider if we should also match on the editorAlias just to be on the safe side [JOV] + const existingData = data.values?.find((x) => x.alias === defaultDataItem.alias); + if (existingData) { + values.push(existingData); + continue; + } + + // If the data is not in the existing data, then we want to add the default data if it exists. + const existingDefaultData = this.#settingsDefaultData.find((x) => x.alias === defaultDataItem.alias); + if (existingDefaultData) { + values.push(existingDefaultData); + } + } + + this._data.updatePersisted({ values }); + this._data.updateCurrent({ values }); } public getPropertyDefaultValue(alias: string) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/import/import-dictionary-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/import/import-dictionary-modal.element.ts index 9e4f850bb3..cb8b9f1186 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/import/import-dictionary-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/import/import-dictionary-modal.element.ts @@ -175,14 +175,14 @@ export class UmbImportDictionaryModalLayout extends UmbModalBaseElement<
- ${this.localize.term('formFileUpload_pickFile')} + ${this.localize.term('dictionary_pickFile')} + required-message=${this.localize.term('dictionary_pickFileRequired')}>
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/structure/document-type-structure.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/structure/document-type-structure.server.data-source.ts index 808fd08c6b..7fe5488195 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/structure/document-type-structure.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/structure/document-type-structure.server.data-source.ts @@ -7,7 +7,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; /** * - + * @class UmbDocumentTypeStructureServerDataSource * @augments {UmbContentTypeStructureServerDataSourceBase} */ @@ -20,10 +20,10 @@ export class UmbDocumentTypeStructureServerDataSource extends UmbContentTypeStru } } -const getAllowedChildrenOf = (unique: string | null) => { +const getAllowedChildrenOf = (unique: string | null, parentContentUnique: string | null) => { if (unique) { // eslint-disable-next-line local-rules/no-direct-api-import - return DocumentTypeService.getDocumentTypeByIdAllowedChildren({ id: unique }); + return DocumentTypeService.getDocumentTypeByIdAllowedChildren({ id: unique, parentContentKey: parentContentUnique ?? undefined }); } else { // eslint-disable-next-line local-rules/no-direct-api-import return DocumentTypeService.getDocumentTypeAllowedAtRoot({}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts index be615b0227..64fa61eb29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts @@ -1,110 +1,12 @@ -import { UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT } from './document-type-workspace.context-token.js'; -import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components'; -import { umbFocus, UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/icon'; -import type { UUITextareaElement } from '@umbraco-cms/backoffice/external/uui'; -import { umbBindToValidation } from '@umbraco-cms/backoffice/validation'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; @customElement('umb-document-type-workspace-editor') export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { - @state() - private _name?: string; - - @state() - private _alias?: string; - - @state() - private _description?: string; - - @state() - private _icon?: string; - - @state() - private _isNew?: boolean; - - #workspaceContext?: typeof UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT.TYPE; - - constructor() { - super(); - - this.consumeContext(UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT, (instance) => { - this.#workspaceContext = instance; - this.#observeDocumentType(); - }); - } - - #observeDocumentType() { - if (!this.#workspaceContext) return; - this.observe(this.#workspaceContext.name, (name) => (this._name = name), '_observeName'); - this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias), '_observeAlias'); - this.observe( - this.#workspaceContext.description, - (description) => (this._description = description), - '_observeDescription', - ); - this.observe(this.#workspaceContext.icon, (icon) => (this._icon = icon), '_observeIcon'); - this.observe(this.#workspaceContext.isNew, (isNew) => (this._isNew = isNew), '_observeIsNew'); - } - - private async _handleIconClick() { - const [alias, color] = this._icon?.replace('color-', '')?.split(' ') ?? []; - const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - const modalContext = modalManager.open(this, UMB_ICON_PICKER_MODAL, { - value: { - icon: alias, - color: color, - }, - }); - - modalContext?.onSubmit().then((saved) => { - if (saved.icon && saved.color) { - this.#workspaceContext?.setIcon(`${saved.icon} color-${saved.color}`); - } else if (saved.icon) { - this.#workspaceContext?.setIcon(saved.icon); - } - }); - } - - #onNameAndAliasChange(event: InputEvent & { target: UmbInputWithAliasElement }) { - this.#workspaceContext?.setName(event.target.value ?? ''); - this.#workspaceContext?.setAlias(event.target.alias ?? ''); - } - - #onDescriptionChange(event: InputEvent & { target: UUITextareaElement }) { - this.#workspaceContext?.setDescription(event.target.value.toString() ?? ''); - } - override render() { return html` - + `; } @@ -116,39 +18,6 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { width: 100%; height: 100%; } - - #header { - display: flex; - flex: 1 1 auto; - gap: var(--uui-size-space-2); - } - - #editors { - display: flex; - flex: 1 1 auto; - flex-direction: column; - gap: var(--uui-size-space-1); - } - - #name { - width: 100%; - } - - #description { - width: 100%; - --uui-input-height: var(--uui-size-8); - --uui-input-border-color: transparent; - } - - #description:hover { - --uui-input-border-color: var(--uui-color-border); - } - - #icon { - font-size: var(--uui-size-8); - height: 60px; - width: 60px; - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts index 25b50fd7d8..b3cf273c8f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts @@ -56,12 +56,12 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { override async firstUpdated() { if (this._documentTypeUnique) { - this.#retrieveAllowedDocumentTypesOf(this._documentTypeUnique); + this.#retrieveAllowedDocumentTypesOf(this._documentTypeUnique, this._documentUnique || null); } } - async #retrieveAllowedDocumentTypesOf(unique: string | null) { - const { data } = await this.#documentTypeStructureRepository.requestAllowedChildrenOf(unique); + async #retrieveAllowedDocumentTypesOf(unique: string | null, parentContentUnique: string | null) { + const { data } = await this.#documentTypeStructureRepository.requestAllowedChildrenOf(unique, parentContentUnique); if (data && data.items) { this._allowedDocumentTypes = data.items; @@ -69,7 +69,7 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { } #onPopoverToggle(event: ToggleEvent) { - // TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet. + // TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this._popoverOpen = event.newState === 'open'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.element.ts index 1dbf684c99..7418d23371 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.element.ts @@ -47,15 +47,15 @@ export class UmbDocumentCreateOptionsModalElement extends UmbModalBaseElement< const parentUnique = this.data?.parent.unique; const documentTypeUnique = this.data?.documentType?.unique || null; - this.#retrieveAllowedDocumentTypesOf(documentTypeUnique); + this.#retrieveAllowedDocumentTypesOf(documentTypeUnique, parentUnique || null); if (parentUnique) { this.#retrieveHeadline(parentUnique); } } - async #retrieveAllowedDocumentTypesOf(unique: string | null) { - const { data } = await this.#documentTypeStructureRepository.requestAllowedChildrenOf(unique); + async #retrieveAllowedDocumentTypesOf(unique: string | null, parentContentUnique: string | null) { + const { data } = await this.#documentTypeStructureRepository.requestAllowedChildrenOf(unique, parentContentUnique); if (data) { // TODO: implement pagination, or get 1000? diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts index 6c4cf7b4b2..cc11edeb22 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts @@ -12,6 +12,7 @@ import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS, } from '@umbraco-cms/backoffice/recycle-bin'; +import { UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS } from '@umbraco-cms/backoffice/entity-action'; export const manifests: Array = [ { @@ -70,6 +71,9 @@ export const manifests: Array = [ alias: 'Umb.Condition.UserPermission.Document', allOf: [UMB_USER_PERMISSION_DOCUMENT_DELETE], }, + { + alias: UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS, + }, ], }, ...bulkTrashManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/manifests.ts index 2f736381e6..24b0612e07 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/manifests.ts @@ -13,6 +13,7 @@ export const manifests: Array = [ meta: { icon: 'icon-history', label: '#actions_rollback', + additionalOptions: true, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 547247c7b7..2134817942 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -35,7 +35,7 @@ import { import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type'; import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; -import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +import { ensurePathEndsWithSlash, UmbDeprecation } from '@umbraco-cms/backoffice/utils'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; type ContentModel = UmbDocumentDetailModel; @@ -289,7 +289,8 @@ export class UmbDocumentWorkspaceContext const appContext = await this.getContext(UMB_APP_CONTEXT); - const previewUrl = new URL(appContext.getBackofficePath() + '/preview', appContext.getServerUrl()); + const backofficePath = appContext.getBackofficePath(); + const previewUrl = new URL(ensurePathEndsWithSlash(backofficePath) + 'preview', appContext.getServerUrl()); previewUrl.searchParams.set('id', unique); if (culture && culture !== UMB_INVARIANT_CULTURE) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts index b6d36b6003..54bc10cf90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts @@ -70,7 +70,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { return html`
From: - { (e.target as HTMLInputElement).showPicker(); }} @@ -78,11 +78,11 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { type="date" label="From" .max=${this.#logViewerContext?.today ?? ''} - .value=${this._startDate} /> + .value=${this._startDate}>
To: - { (e.target as HTMLInputElement).showPicker(); }} @@ -91,7 +91,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { label="To" .min=${this._startDate} .max=${this.#logViewerContext?.today ?? ''} - .value=${this._endDate} /> + .value=${this._endDate}>
`; } @@ -104,30 +104,14 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { flex-direction: column; gap: var(--uui-size-space-3); } - - input { - font-family: inherit; - padding: var(--uui-size-1) var(--uui-size-space-3); - font-size: inherit; - color: inherit; - border-radius: 0; - box-sizing: border-box; - border: none; - background: none; + umb-input-date { width: 100%; - outline: none; - position: relative; - border-bottom: 2px solid transparent; - } - - /* find out better validation for that */ - input:out-of-range { - border-color: var(--uui-color-danger); } :host([horizontal]) .input-container { display: flex; align-items: baseline; + gap: var(--uui-size-space-3); } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-level-tag.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-level-tag.element.ts index c360fa7b2e..8b84e93388 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-level-tag.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-level-tag.element.ts @@ -24,7 +24,7 @@ export class UmbLogViewerLevelTagElement extends LitElement { Error: { look: 'primary', color: 'danger' }, Fatal: { look: 'primary', - style: 'background-color: var(--umb-log-viewer-fatal-color); color: var(--uui-color-surface)', + style: 'background-color: var(--umb-log-viewer-fatal-color)', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts index c3c2f927f6..d2384cdde3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts @@ -26,10 +26,10 @@ export class UmbMediaTypeStructureServerDataSource extends UmbContentTypeStructu } } -const getAllowedChildrenOf = (unique: string | null) => { +const getAllowedChildrenOf = (unique: string | null, parentContentUnique: string | null) => { if (unique) { // eslint-disable-next-line local-rules/no-direct-api-import - return MediaTypeService.getMediaTypeByIdAllowedChildren({ id: unique }); + return MediaTypeService.getMediaTypeByIdAllowedChildren({ id: unique, parentContentKey: parentContentUnique ?? undefined }); } else { // eslint-disable-next-line local-rules/no-direct-api-import return MediaTypeService.getMediaTypeAllowedAtRoot({}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts index 10c68fbe84..8a9fe04b18 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts @@ -1,112 +1,12 @@ -import type { UmbMediaTypeWorkspaceContext } from './media-type-workspace.context.js'; -import { UMB_MEDIA_TYPE_WORKSPACE_CONTEXT } from './media-type-workspace.context-token.js'; -import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/icon'; -import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components'; -import type { UUITextareaElement } from '@umbraco-cms/backoffice/external/uui'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-media-type-workspace-editor') export class UmbMediaTypeWorkspaceEditorElement extends UmbLitElement { - @state() - private _name?: string; - - @state() - private _description?: string; - - @state() - private _alias?: string; - - @state() - private _aliasLocked = true; - - @state() - private _icon?: string; - - @state() - private _isNew?: boolean; - - #workspaceContext?: UmbMediaTypeWorkspaceContext; - - constructor() { - super(); - - this.consumeContext(UMB_MEDIA_TYPE_WORKSPACE_CONTEXT, (context) => { - this.#workspaceContext = context; - this.#observeMediaType(); - }); - } - - #observeMediaType() { - if (!this.#workspaceContext) return; - this.observe(this.#workspaceContext.name, (name) => (this._name = name), '_observeName'); - this.observe( - this.#workspaceContext.description, - (description) => (this._description = description), - '_observeDescription', - ); - this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias), '_observeAlias'); - this.observe(this.#workspaceContext.icon, (icon) => (this._icon = icon), '_observeIcon'); - this.observe(this.#workspaceContext.isNew, (isNew) => (this._isNew = isNew), '_observeIsNew'); - } - - private async _handleIconClick() { - const [alias, color] = this._icon?.replace('color-', '')?.split(' ') ?? []; - - const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - const modalContext = modalManager.open(this, UMB_ICON_PICKER_MODAL, { - value: { - icon: alias, - color: color, - }, - }); - - modalContext?.onSubmit().then((saved) => { - if (saved.icon && saved.color) { - this.#workspaceContext?.setIcon(`${saved.icon} color-${saved.color}`); - } else if (saved.icon) { - this.#workspaceContext?.setIcon(saved.icon); - } - }); - } - - #onNameAndAliasChange(event: InputEvent & { target: UmbInputWithAliasElement }) { - this.#workspaceContext?.setName(event.target.value ?? ''); - this.#workspaceContext?.setAlias(event.target.alias ?? ''); - } - - #onDescriptionChange(event: InputEvent & { target: UUITextareaElement }) { - this.#workspaceContext?.setDescription(event.target.value.toString() ?? ''); - } - override render() { return html` - + `; } @@ -118,39 +18,6 @@ export class UmbMediaTypeWorkspaceEditorElement extends UmbLitElement { width: 100%; height: 100%; } - - #header { - display: flex; - flex: 1 1 auto; - gap: var(--uui-size-space-2); - } - - #editors { - display: flex; - flex: 1 1 auto; - flex-direction: column; - gap: var(--uui-size-space-1); - } - - #name { - width: 100%; - } - - #description { - width: 100%; - --uui-input-height: var(--uui-size-8); - --uui-input-border-color: transparent; - } - - #description:hover { - --uui-input-border-color: var(--uui-color-border); - } - - #icon { - font-size: var(--uui-size-8); - height: 60px; - width: 60px; - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts index 1272eb50e1..0e05d732fc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts @@ -53,11 +53,11 @@ export class UmbCreateMediaCollectionActionElement extends UmbLitElement { } override async firstUpdated() { - this.#retrieveAllowedMediaTypesOf(this._mediaTypeUnique ?? ''); + this.#retrieveAllowedMediaTypesOf(this._mediaTypeUnique ?? '', this._mediaUnique || null); } - async #retrieveAllowedMediaTypesOf(unique: string | null) { - const { data } = await this.#mediaTypeStructureRepository.requestAllowedChildrenOf(unique); + async #retrieveAllowedMediaTypesOf(unique: string | null, parentContentUnique: string | null) { + const { data } = await this.#mediaTypeStructureRepository.requestAllowedChildrenOf(unique, parentContentUnique); if (data && data.items) { this._allowedMediaTypes = data.items; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index 4a1bc9e3c6..b58069b537 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -226,7 +226,7 @@ export class UmbDropzoneManager extends UmbControllerBase { async #getMediaTypeOptions(item: UmbUploadableItem): Promise> { // Check the parent which children media types are allowed const parent = item.parentUnique ? await this.#mediaDetailRepository.requestByUnique(item.parentUnique) : null; - const allowedChildren = await this.#getAllowedChildrenOf(parent?.data?.mediaType.unique ?? null); + const allowedChildren = await this.#getAllowedChildrenOf(parent?.data?.mediaType.unique ?? null, item.parentUnique); const extension = item.temporaryFile?.file.name.split('.').pop() ?? null; @@ -255,7 +255,7 @@ export class UmbDropzoneManager extends UmbControllerBase { return availableMediaTypes; } - async #getAllowedChildrenOf(mediaTypeUnique: string | null) { + async #getAllowedChildrenOf(mediaTypeUnique: string | null, parentUnique: string | null) { //Check if we already got information on this media type. const allowed = this.#allowedChildrenOf .getValue() @@ -263,7 +263,7 @@ export class UmbDropzoneManager extends UmbControllerBase { if (allowed) return allowed; // Request information on this media type. - const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(mediaTypeUnique); + const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(mediaTypeUnique, parentUnique); if (!data) throw new Error('Parent media type does not exists'); this.#allowedChildrenOf.appendOne({ mediaTypeUnique, allowedChildren: data.items }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts index f25c1b5759..a7e3fd544f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts @@ -26,15 +26,15 @@ export class UmbMediaCreateOptionsModalElement extends UmbModalBaseElement< const mediaUnique = this.data?.parent.unique; const mediaTypeUnique = this.data?.mediaType?.unique || null; - this.#retrieveAllowedMediaTypesOf(mediaTypeUnique); + this.#retrieveAllowedMediaTypesOf(mediaTypeUnique, mediaUnique || null); if (mediaUnique) { this.#retrieveHeadline(mediaUnique); } } - async #retrieveAllowedMediaTypesOf(unique: string | null) { - const { data } = await this.#mediaTypeStructureRepository.requestAllowedChildrenOf(unique); + async #retrieveAllowedMediaTypesOf(unique: string | null, parentContentUnique: string | null) { + const { data } = await this.#mediaTypeStructureRepository.requestAllowedChildrenOf(unique, parentContentUnique); if (data) { // TODO: implement pagination, or get 1000? diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts index 0b45165f4c..4c5d7cce3b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts @@ -36,7 +36,7 @@ export class UmbMediaPickerCreateItemElement extends UmbLitElement { async #getAllowedMediaTypes() { const mediaType = await this.#getNodeMediaType(); - const { data: allowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(mediaType); + const { data: allowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(mediaType, this._node); this._allowedMediaTypes = allowedMediaTypes?.items ?? []; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts index 65b3693975..144684b99c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts @@ -6,6 +6,7 @@ import { import { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE, UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS } from '../constants.js'; import { UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS } from '../../reference/constants.js'; import { manifests as bulkTrashManifests } from './bulk-trash/manifests.js'; +import { UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS } from '@umbraco-cms/backoffice/entity-action'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS, @@ -55,6 +56,11 @@ export const manifests: Array = [ meta: { recycleBinRepositoryAlias: UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS, }, + conditions: [ + { + alias: UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS, + }, + ], }, ...bulkTrashManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts index 521a3a54c5..af1dc269e9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts @@ -1,107 +1,12 @@ -import { UMB_MEMBER_TYPE_WORKSPACE_CONTEXT } from './member-type-workspace.context-token.js'; -import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components'; -import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import type { UUITextareaElement } from '@umbraco-cms/backoffice/external/uui'; -import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/icon'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-member-type-workspace-editor') export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement { - @state() - private _name?: string; - - @state() - private _description?: string; - - @state() - private _alias?: string; - - @state() - private _icon?: string; - - @state() - private _isNew?: boolean; - - #workspaceContext?: typeof UMB_MEMBER_TYPE_WORKSPACE_CONTEXT.TYPE; - - constructor() { - super(); - - this.consumeContext(UMB_MEMBER_TYPE_WORKSPACE_CONTEXT, (instance) => { - this.#workspaceContext = instance; - this.#observeMemberType(); - }); - } - - #observeMemberType() { - if (!this.#workspaceContext) return; - this.observe(this.#workspaceContext.name, (name) => (this._name = name), '_observeName'); - this.observe( - this.#workspaceContext.description, - (description) => (this._description = description), - '_observeDescription', - ); - this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias), '_observeAlias'); - this.observe(this.#workspaceContext.icon, (icon) => (this._icon = icon), '_observeIcon'); - this.observe(this.#workspaceContext.isNew, (isNew) => (this._isNew = isNew), '_observeIsNew'); - } - - private async _handleIconClick() { - const [alias, color] = this._icon?.replace('color-', '')?.split(' ') ?? []; - const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - const modalContext = modalManager.open(this, UMB_ICON_PICKER_MODAL, { - value: { - icon: alias, - color: color, - }, - }); - - modalContext?.onSubmit().then((saved) => { - if (saved.icon && saved.color) { - this.#workspaceContext?.set('icon', `${saved.icon} color-${saved.color}`); - } else if (saved.icon) { - this.#workspaceContext?.set('icon', saved.icon); - } - }); - } - - #onNameAndAliasChange(event: InputEvent & { target: UmbInputWithAliasElement }) { - this.#workspaceContext?.setName(event.target.value ?? ''); - this.#workspaceContext?.setAlias(event.target.alias ?? ''); - } - - #onDescriptionChange(event: InputEvent & { target: UUITextareaElement }) { - this.#workspaceContext?.setDescription(event.target.value.toString() ?? ''); - } - override render() { return html` - + `; } @@ -113,51 +18,6 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement { width: 100%; height: 100%; } - - #header { - display: flex; - flex: 1 1 auto; - gap: var(--uui-size-space-2); - } - - #editors { - display: flex; - flex: 1 1 auto; - flex-direction: column; - gap: var(--uui-size-space-1); - } - - #name { - width: 100%; - flex: 1 1 auto; - align-items: center; - } - - #description { - width: 100%; - --uui-input-height: var(--uui-size-8); - --uui-input-border-color: transparent; - } - - #description:hover { - --uui-input-border-color: var(--uui-color-border); - } - - #alias-lock { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - } - #alias-lock uui-icon { - margin-bottom: 2px; - } - - #icon { - font-size: var(--uui-size-8); - height: 60px; - width: 60px; - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts index 2ef18e11f1..f47fe571ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts @@ -69,7 +69,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement x.unique).filter((x) => x && !isUmbracoFolder(x)) as Array) ?? []; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/publish-cache/dashboard-published-status.element.ts b/src/Umbraco.Web.UI.Client/src/packages/publish-cache/dashboard-published-status.element.ts index eeead01e78..f05a6bf488 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/publish-cache/dashboard-published-status.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/publish-cache/dashboard-published-status.element.ts @@ -14,6 +14,8 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement { @state() private _buttonStateRebuild: UUIButtonState = undefined; + #isFirstRebuildStatusPoll: boolean = true; + //Reload private async _reloadMemoryCache() { this._buttonStateReload = 'waiting'; @@ -37,12 +39,31 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement { // Rebuild private async _rebuildDatabaseCache() { + this._buttonStateRebuild = 'waiting'; const { error } = await tryExecuteAndNotify(this, PublishedCacheService.postPublishedCacheRebuild()); if (error) { this._buttonStateRebuild = 'failed'; } else { - this._buttonStateRebuild = 'success'; + this.#isFirstRebuildStatusPoll = true; + this._pollForRebuildDatabaseCacheStatus(); + } + } + + private async _pollForRebuildDatabaseCacheStatus() { + //Checking the server after 1 second and then every 5 seconds to see if the database cache is still rebuilding. + while (this._buttonStateRebuild === 'waiting') { + await new Promise((resolve) => setTimeout(resolve, this.#isFirstRebuildStatusPoll ? 1000 : 5000)); + this.#isFirstRebuildStatusPoll = false; + const { data, error } = await tryExecuteAndNotify(this, PublishedCacheService.getPublishedCacheRebuildStatus()); + if (error || !data) { + this._buttonStateRebuild = 'failed'; + return; + } + + if (!data.isRebuilding) { + this._buttonStateRebuild = 'success'; + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-menu.element.ts index e30cc9aefc..089ac20ade 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-menu.element.ts @@ -129,7 +129,7 @@ export class UmbTiptapToolbarMenuElement extends UmbLitElement { `, () => html` - + ${label} @@ -146,11 +146,12 @@ export class UmbTiptapToolbarMenuElement extends UmbLitElement { --uui-button-font-weight: normal; --uui-menu-item-flat-structure: 1; - margin-inline-start: var(--uui-size-space-1); + margin-left: var(--uui-size-space-1); + margin-bottom: var(--uui-size-space-1); } uui-button > uui-symbol-expand { - margin-left: var(--uui-size-space-4); + margin-left: var(--uui-size-space-2); } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/embedded-media.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/embedded-media.tiptap-toolbar-api.ts index 0a91f0801b..b691bb7c7a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/embedded-media.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/embedded-media.tiptap-toolbar-api.ts @@ -5,8 +5,6 @@ import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapToolbarEmbeddedMediaExtensionApi extends UmbTiptapToolbarElementApiBase { - override isActive = (editor: Editor) => editor.isActive(umbEmbeddedMedia.name) === true; - override async execute(editor?: Editor) { const data = { constrain: false, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts index 5f9756f9af..43267a31c9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts @@ -87,6 +87,8 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement this.#setValue(tmpValue); this.#syncViewModel(); } + + this.requestUpdate('_extensions'); }, '_observeBlocks', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts index 1ce96bb9ff..46525299dc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts @@ -141,16 +141,18 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
-
- ${when( - this._availableExtensions.length === 0, - () => - html`There are no toolbar extensions to show`, - () => repeat(this._availableExtensions, (item) => this.#renderAvailableItem(item)), - )} -
+ +
+ ${when( + this._availableExtensions.length === 0, + () => + html`There are no toolbar extensions to show`, + () => repeat(this._availableExtensions, (item) => this.#renderAvailableItem(item)), + )} +
+
`; } @@ -158,24 +160,23 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement #renderAvailableItem(item: UmbTiptapToolbarExtension) { const forbidden = !this.#context.isExtensionEnabled(item.alias); const inUse = this.#context.isExtensionInUse(item.alias); - return inUse || forbidden - ? nothing - : html` - this.#onClick(item)} - @dragstart=${(e: DragEvent) => this.#onDragStart(e, item.alias)} - @dragend=${this.#onDragEnd}> -
- ${when(item.icon, () => html``)} - ${this.localize.string(item.label)} -
-
- `; + if (inUse || forbidden) return nothing; + return html` + this.#onClick(item)} + @dragstart=${(e: DragEvent) => this.#onDragStart(e, item.alias)} + @dragend=${this.#onDragEnd}> +
+ ${when(item.icon, () => html``)} + ${this.localize.string(item.label)} +
+
+ `; } #renderDesigner() { @@ -273,27 +274,52 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement #renderItem(alias: string, rowIndex = 0, groupIndex = 0, itemIndex = 0) { const item = this.#context?.getExtensionByAlias(alias); if (!item) return nothing; + const forbidden = !this.#context?.isExtensionEnabled(item.alias); - return html` - this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])} - @dragend=${this.#onDragEnd} - @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}> -
- ${when( - item.icon, - () => html``, - () => html`${this.localize.string(item.label)}`, - )} -
-
- `; + + switch (item.kind) { + case 'menu': + return html` + this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])} + @dragend=${this.#onDragEnd} + @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}> +
+ ${this.localize.string(item.label)} +
+ +
+ `; + + case 'button': + default: + return html` + this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])} + @dragend=${this.#onDragEnd} + @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}> +
+ ${when( + item.icon, + () => html``, + () => html`${this.localize.string(item.label)}`, + )} +
+
+ `; + } } static override readonly styles = [ @@ -339,6 +365,10 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement } } + uui-scroll-container { + max-height: 350px; + } + .available-items { display: flex; flex-wrap: wrap; @@ -466,6 +496,10 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement display: flex; gap: var(--uui-size-1); } + + uui-symbol-expand { + margin-left: var(--uui-size-space-2); + } } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts index 7214504198..93d78d4208 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts @@ -39,8 +39,8 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase { - const _extensions = extensions.map((ext) => ({ - alias: ext.alias, - label: ext.meta.label, - icon: ext.meta.icon, - dependencies: ext.forExtensions, - })); + const _extensions = extensions + .sort((a, b) => a.alias.localeCompare(b.alias)) + .map((ext) => ({ + kind: (ext.kind as string) ?? 'button', + alias: ext.alias, + label: ext.meta.label, + icon: ext.meta.icon, + dependencies: ext.forExtensions, + })); this.#extensions.setValue(_extensions); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts index 305cd36967..448accfa2c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts @@ -1,4 +1,5 @@ export type UmbTiptapToolbarExtension = { + kind?: string; alias: string; label: string; icon: string; diff --git a/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts b/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts index acee711254..730423de71 100644 --- a/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts +++ b/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts @@ -1,5 +1,6 @@ +import { css, customElement, html, nothing, property, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { css, CSSResultGroup, html, nothing, PropertyValueMap, customElement, property, when } from '@umbraco-cms/backoffice/external/lit'; +import type { CSSResultGroup, PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; /** * The auth layout component. @@ -24,200 +25,208 @@ import { css, CSSResultGroup, html, nothing, PropertyValueMap, customElement, pr * @cssprop --umb-login-button-border-radius - The border-radius of the buttons (default: 45px) * @cssprop --umb-login-curves-color - The color of the curves (default: #f5c1bc) * @cssprop --umb-login-curves-display - The display of the curves (default: inline) + * @cssprop --umb-logo-width - The width of the logo (default: auto) + * @cssprop --umb-logo-height - The height of the logo (default: 55px) + * @cssprop --umb-logo-top - The top position of the logo (default: 24px) + * @cssprop --umb-logo-left - The left position of the logo (default: 24px) + * @cssprop --umb-logo-display - The display of the logo (default: block) */ @customElement('umb-auth-layout') export class UmbAuthLayoutElement extends UmbLitElement { - @property({ attribute: 'background-image' }) - backgroundImage?: string; + @property({ attribute: 'background-image' }) + backgroundImage?: string; - @property({ attribute: 'logo-image' }) - logoImage?: string; + @property({ attribute: 'logo-image' }) + logoImage?: string; - @property({ attribute: 'logo-image-alternative' }) - logoImageAlternative?: string; + @property({ attribute: 'logo-image-alternative' }) + logoImageAlternative?: string; - protected updated(_changedProperties: PropertyValueMap | Map): void { - super.updated(_changedProperties); + protected updated(_changedProperties: PropertyValueMap | Map): void { + super.updated(_changedProperties); - if (_changedProperties.has('backgroundImage')) { - this.style.setProperty('--logo-alternative-display', this.backgroundImage ? 'none' : 'unset'); - this.style.setProperty('--image', `url('${this.backgroundImage}') no-repeat center center/cover`); - } - } + if (_changedProperties.has('backgroundImage')) { + this.style.setProperty('--logo-alternative-display', this.backgroundImage ? 'none' : 'unset'); + this.style.setProperty('--image', `url('${this.backgroundImage}') no-repeat center center/cover`); + } + } - #renderImageContainer() { - if (!this.backgroundImage) return nothing; + #renderImageContainer() { + if (!this.backgroundImage) return nothing; - return html` -
-
- - - - - - + return html` +
+
+ + + + + + - ${when( - this.logoImage, - () => html`` - )} -
-
- `; - } + ${when( + this.logoImage, + (logoImage) => html`` + )} +
+
+ `; + } - #renderContent() { - return html` -
-
- -
-
- `; - } + #renderContent() { + return html` +
+
+ +
+
+ `; + } - render() { - return html` -
- ${this.#renderImageContainer()} ${this.#renderContent()} -
- ${when( - this.logoImageAlternative, - () => html`` - )} - `; - } + render() { + return html` +
+ ${this.#renderImageContainer()} ${this.#renderContent()} +
+ ${when( + this.logoImageAlternative, + (logoImageAlternative) => + html`` + )} + `; + } - static styles: CSSResultGroup = [ - css` - :host { - --uui-color-interactive: var(--umb-login-primary-color, #283a97); - --uui-button-border-radius: var(--umb-login-button-border-radius, 45px); - --uui-color-default: var(--uui-color-interactive); - --uui-button-height: 42px; - --uui-select-height: 38px; + static styles: CSSResultGroup = [ + css` + :host { + --uui-color-interactive: var(--umb-login-primary-color, #283a97); + --uui-button-border-radius: var(--umb-login-button-border-radius, 45px); + --uui-color-default: var(--uui-color-interactive); + --uui-button-height: 42px; + --uui-select-height: 38px; - --input-height: 40px; - --header-font-size: var(--umb-login-header-font-size, 3rem); - --header-secondary-font-size: var(--umb-login-header-secondary-font-size, 2.4rem); - --curves-color: var(--umb-login-curves-color, #f5c1bc); - --curves-display: var(--umb-login-curves-display, inline); + --input-height: 40px; + --header-font-size: var(--umb-login-header-font-size, 3rem); + --header-secondary-font-size: var(--umb-login-header-secondary-font-size, 2.4rem); + --curves-color: var(--umb-login-curves-color, #f5c1bc); + --curves-display: var(--umb-login-curves-display, inline); - display: block; - background: var(--umb-login-background, #f4f4f4); - color: var(--umb-login-text-color, #000); - } + display: block; + background: var(--umb-login-background, #f4f4f4); + color: var(--umb-login-text-color, #000); + } - #main-no-image, - #main { - max-width: 1920px; - display: flex; - justify-content: center; - height: 100vh; - padding: 8px; - box-sizing: border-box; - margin: 0 auto; - } + #main-no-image, + #main { + max-width: 1920px; + display: flex; + justify-content: center; + height: 100vh; + padding: 8px; + box-sizing: border-box; + margin: 0 auto; + } - #image-container { - display: var(--umb-login-image-display, none); - width: 100%; - } + #image-container { + display: var(--umb-login-image-display, none); + width: 100%; + } - #content-container { - background: var(--umb-login-content-background, none); - display: var(--umb-login-content-display, flex); - width: var(--umb-login-content-width, 100%); - height: var(--umb-login-content-height, 100%); - box-sizing: border-box; - overflow: auto; - border-radius: var(--umb-login-content-border-radius, 0); - } + #content-container { + background: var(--umb-login-content-background, none); + display: var(--umb-login-content-display, flex); + width: var(--umb-login-content-width, 100%); + height: var(--umb-login-content-height, 100%); + box-sizing: border-box; + overflow: auto; + border-radius: var(--umb-login-content-border-radius, 0); + } - #content { - max-width: 360px; - margin: auto; - width: 100%; - } + #content { + max-width: 360px; + margin: auto; + width: 100%; + } - #image { - background: var(--umb-login-image, var(--image)); - width: 100%; - height: 100%; - border-radius: var(--umb-login-image-border-radius, 38px); - position: relative; - overflow: hidden; - color: var(--curves-color); - } + #image { + background: var(--umb-login-image, var(--image)); + width: 100%; + height: 100%; + border-radius: var(--umb-login-image-border-radius, 38px); + position: relative; + overflow: hidden; + color: var(--curves-color); + } - #image svg { - position: absolute; - width: 45%; - height: fit-content; - display: var(--curves-display); - } + #image svg { + position: absolute; + width: 45%; + height: fit-content; + display: var(--curves-display); + } - #curve-top { - top: 0; - right: 0; - } + #curve-top { + top: 0; + right: 0; + } - #curve-bottom { - bottom: 0; - left: 0; - } + #curve-bottom { + bottom: 0; + left: 0; + } - #logo-on-image, - #logo-on-background { - position: absolute; - top: 24px; - left: 24px; - height: 55px; - } + #logo-on-image, + #logo-on-background { + position: absolute; + display: var(--umb-logo-display, block); + top: var(--umb-logo-top, 24px); + left: var(--umb-logo-left, 24px); + width: var(--umb-logo-width, auto); + height: var(--umb-logo-height, 55px); + } - @media only screen and (min-width: 900px) { - :host { - --header-font-size: var(--umb-login-header-font-size-large, 4rem); - } + @media only screen and (min-width: 900px) { + :host { + --header-font-size: var(--umb-login-header-font-size-large, 4rem); + } - #main { - padding: 32px; - padding-right: 0; - align-items: var(--umb-login-align-items, unset); - } + #main { + padding: 32px; + padding-right: 0; + align-items: var(--umb-login-align-items, unset); + } - #image-container { - display: var(--umb-login-image-display, block); - } + #image-container { + display: var(--umb-login-image-display, block); + } - #content-container { - display: var(--umb-login-content-display, flex); - padding: 16px; - } + #content-container { + display: var(--umb-login-content-display, flex); + padding: 16px; + } - #logo-on-background { - display: var(--logo-alternative-display); - } - } - `, - ]; + #logo-on-background { + display: var(--logo-alternative-display); + } + } + `, + ]; } declare global { - interface HTMLElementTagNameMap { - 'umb-auth-layout': UmbAuthLayoutElement; - } + interface HTMLElementTagNameMap { + 'umb-auth-layout': UmbAuthLayoutElement; + } } diff --git a/src/Umbraco.Web.UI.Login/src/mocks/handlers/backoffice.handlers.ts b/src/Umbraco.Web.UI.Login/src/mocks/handlers/backoffice.handlers.ts index 434d225705..bce35fedf3 100644 --- a/src/Umbraco.Web.UI.Login/src/mocks/handlers/backoffice.handlers.ts +++ b/src/Umbraco.Web.UI.Login/src/mocks/handlers/backoffice.handlers.ts @@ -1,6 +1,7 @@ import { HttpHandler, http, HttpResponse } from "msw"; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; import logoUrl from '../../../../Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo.svg'; +import logoAlternativeUrl from '../../../../Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_blue.svg'; import loginLogoUrl from '../../../../Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_light.svg'; import loginLogoAlternativeUrl from '../../../../Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/logo_dark.svg'; import loginBackgroundUrl from '../../../../Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/login.jpg'; @@ -10,6 +11,17 @@ export const handlers: HttpHandler[] = [ const imageBuffer = await fetch(logoUrl) .then((res) => res.arrayBuffer()); + return HttpResponse.arrayBuffer(imageBuffer, { + headers: { + 'Content-Length': imageBuffer.byteLength.toString(), + 'Content-Type': 'image/svg+xml' + } + }); + }), + http.get(umbracoPath('/security/back-office/graphics/logo-alternative'), async () => { + const imageBuffer = await fetch(logoAlternativeUrl) + .then((res) => res.arrayBuffer()); + return HttpResponse.arrayBuffer(imageBuffer, { headers: { 'Content-Length': imageBuffer.byteLength.toString(), diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 8ea239c327..4bc42e2d78 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.29", - "@umbraco/playwright-testhelpers": "^15.0.24", + "@umbraco/playwright-testhelpers": "^15.0.32", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.24", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.24.tgz", - "integrity": "sha512-cv7sr3e1vhOoqAKOgj82kKgWY9dCQCnQdP+4rGllM/Dhvup+nSs93XKOAnTc2Fn3ZqhpwA8PDL8Pg9riUpt5JQ==", + "version": "15.0.32", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.32.tgz", + "integrity": "sha512-4wzLTtqbzIc0TokP+/nC/vbKfcboYQFGam6eLzZj4oMQmkBExxv5EBhI06qrpst8/rQc5OK4TTwJAGL3GCuKew==", "dependencies": { "@umbraco/json-models-builders": "2.0.30", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 9c542f9bf0..71dd1e16d3 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.24", + "@umbraco/playwright-testhelpers": "^15.0.32", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts index 00483cdc83..5a784172c2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts @@ -23,7 +23,8 @@ export default defineConfig({ // 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 */ - reporter: process.env.CI ? 'line' : 'html', + //reporter: process.env.CI ? 'line' : 'html', + reporter: process.env.CI ? [['line'], ['junit', {outputFile: 'results/results.xml'}]] : 'html', outputDir: "./results", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts index f36fb09883..78cc3db04d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts @@ -43,7 +43,7 @@ test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = expect(childData[0].variants[0].name).toBe(childContentName); // verify that the child content displays in the tree after reloading children await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickReloadButton(); + await umbracoUi.content.clickReloadChildrenButton(); await umbracoUi.content.clickCaretButtonForContentName(contentName); await umbracoUi.content.doesContentTreeHaveName(childContentName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCustomDataType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCustomDataType.spec.ts index c13538b73f..d605cb1936 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCustomDataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCustomDataType.spec.ts @@ -244,8 +244,8 @@ test('can add string to the multiple text string in the content section', async test('can create content with the custom data type with slider property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - customDataTypeName = 'Slider'; - const customDataTypeId = await umbracoApi.dataType.createSliderDataTyper(customDataTypeName); + customDataTypeName = 'Custom Slider'; + const customDataTypeId = await umbracoApi.dataType.createSliderDataType(customDataTypeName); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); @@ -273,7 +273,7 @@ test('can change slider value in the content section', async ({umbracoApi, umbra "from": sliderValue, "to": sliderValue } - const customDataTypeId = await umbracoApi.dataType.createSliderDataTyper(customDataTypeName); + const customDataTypeId = await umbracoApi.dataType.createSliderDataType(customDataTypeName); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts index 766b26cdf1..d567a0984f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts @@ -83,7 +83,7 @@ test('can create multiple child nodes with different document types', async ({um expect(childData[1].variants[0].name).toBe(secondChildContentName); // verify that the child content displays in the tree after reloading children await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickReloadButton(); + await umbracoUi.content.clickReloadChildrenButton(); await umbracoUi.content.clickCaretButtonForContentName(contentName); await umbracoUi.content.doesContentTreeHaveName(firstChildContentName); await umbracoUi.content.doesContentTreeHaveName(secondChildContentName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithCollections.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithCollections.spec.ts index 17f17d6dab..98868fc1c1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithCollections.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithCollections.spec.ts @@ -62,7 +62,7 @@ test('can create child content in a collection', async ({umbracoApi, umbracoUi}) expect(childData[0].variants[0].name).toBe(firstChildContentName); // verify that the child content displays in collection list after reloading tree await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickReloadButton(); + await umbracoUi.content.clickReloadChildrenButton(); await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.doesDocumentTableColumnNameValuesMatch(expectedNames); @@ -95,7 +95,7 @@ test('can create multiple child nodes in a collection', async ({umbracoApi, umbr expect(childData[1].variants[0].name).toBe(secondChildContentName); // verify that the child content displays in collection list after reloading tree await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickReloadButton(); + await umbracoUi.content.clickReloadChildrenButton(); await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.doesDocumentTableColumnNameValuesMatch(expectedNames); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTiptap.spec.ts index b1097e3c2e..cbac5c752e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTiptap.spec.ts @@ -86,9 +86,9 @@ test('can publish content with RTE Tiptap property editor', async ({umbracoApi, expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); }); -test('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { +test.fixme('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const iconTitle = 'Media picker'; + const iconTitle = 'Media Picker'; const imageName = 'Test Image For Content'; await umbracoApi.media.ensureNameNotExists(imageName); await umbracoApi.media.createDefaultMediaWithImage(imageName); @@ -100,6 +100,7 @@ test('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbrac // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); + // fix this await umbracoUi.content.selectMediaWithName(imageName); await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickMediaCaptionAltTextModalSubmitButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts index 479176b401..e015ea6115 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts @@ -84,13 +84,15 @@ test('can toggle the true/false value in the content ', async ({umbracoApi, umbr test('can toggle the true/false value with the initial state enabled', async ({umbracoApi, umbracoUi}) => { // Arrange const dataTypeId = await umbracoApi.dataType.createTrueFalseDataTypeWithInitialState(customDataTypeName); - const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); - await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act - await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickToggleButton(); await umbracoUi.content.clickSaveButton(); @@ -100,6 +102,9 @@ test('can toggle the true/false value with the initial state enabled', async ({u const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); expect(contentData.values[0].value).toEqual(false); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); }); test('can show the label on for the true/false in the content ', async ({umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/IssueWithScheduledPublishing.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/IssueWithScheduledPublishing.spec.ts new file mode 100644 index 0000000000..c2845760e5 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/IssueWithScheduledPublishing.spec.ts @@ -0,0 +1,42 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +const documentTypeName = "DocumentType"; +const contentName = "Content"; +const languageName = 'Danish'; +let documentTypeId = null; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowVaryByCulture(documentTypeName); + await umbracoApi.document.createDefaultDocumentWithEnglishCulture(contentName, documentTypeId); + await umbracoApi.language.ensureNameNotExists(languageName); + await umbracoApi.language.createDanishLanguage(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.language.ensureNameNotExists(languageName); +}); + +// https://github.com/umbraco/Umbraco-CMS/issues/18555 +test.skip('Can schedule publish after unselecting all languages', async ({umbracoUi}) => { + // Arrange + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + // Open schedule modal and click schedule + await umbracoUi.content.changeDocumentSectionLanguage(languageName); + await umbracoUi.content.goToContentWithName('(' + contentName + ')'); + await umbracoUi.content.enterContentName('Tester'); + await umbracoUi.content.clickViewMoreOptionsButton(); + await umbracoUi.content.clickScheduleButton(); + await umbracoUi.waitForTimeout(500); + await umbracoUi.content.clickSelectAllCheckbox(); + await umbracoUi.waitForTimeout(500); + await umbracoUi.content.clickSelectAllCheckbox(); + await umbracoUi.content.clickButtonWithName(contentName); + + // Assert + await umbracoUi.content.doesSchedulePublishModalButtonContainDisabledTag(false); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ApprovedColor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ApprovedColor.spec.ts index 61b8fbf225..e853b2afa8 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ApprovedColor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ApprovedColor.spec.ts @@ -34,7 +34,7 @@ test('can include label', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dataType.goToDataType(dataTypeName); // Act - await umbracoUi.dataType.clickIncludeLabelsSlider(); + await umbracoUi.dataType.clickIncludeLabelsToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ContentPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ContentPicker.spec.ts index 9cf26d793c..8330c7136a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ContentPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ContentPicker.spec.ts @@ -26,7 +26,7 @@ test('can show open button', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dataType.goToDataType(dataTypeName); // Act - await umbracoUi.dataType.clickShowOpenButtonSlider(); + await umbracoUi.dataType.clickShowOpenButtonToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -43,7 +43,7 @@ test('can ignore user start nodes', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dataType.goToDataType(dataTypeName); // Act - await umbracoUi.dataType.clickIgnoreUserStartNodesSlider(); + await umbracoUi.dataType.clickIgnoreUserStartNodesToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts index 82c0d392e7..f32dd850bf 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts @@ -65,8 +65,9 @@ test('can change property editor in a data type', {tag: '@smoke'}, async ({umbra const updatedEditorName = 'Text Area'; const updatedEditorAlias = 'Umbraco.TextArea'; const updatedEditorUiAlias = 'Umb.PropertyEditorUi.TextArea'; + const maxChars = 999; - await umbracoApi.dataType.createTextstringDataType(dataTypeName); + await umbracoApi.dataType.createTextstringDataType(dataTypeName, maxChars); expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); // Act @@ -81,6 +82,9 @@ test('can change property editor in a data type', {tag: '@smoke'}, async ({umbra const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); expect(dataTypeData.editorAlias).toBe(updatedEditorAlias); expect(dataTypeData.editorUiAlias).toBe(updatedEditorUiAlias); + + const maxCharsSetting = dataTypeData.values.find((x: {alias: string, value: unknown}) => x.alias === 'maxChars'); + expect(maxCharsSetting.value, 'Stored configuration should be transferred').toBe(maxChars); }); test('cannot create a data type without selecting the property editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts index 1454cb6f34..ba07bf8123 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts @@ -37,9 +37,9 @@ test('can rename a data type folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickActionsMenuForDataType(wrongDataTypeFolderName); - await umbracoUi.dataType.clickRenameFolderThreeDotsButton(); + await umbracoUi.dataType.clickRenameFolderButton(); await umbracoUi.dataType.enterFolderName(dataTypeFolderName); - await umbracoUi.dataType.clickConfirmRenameFolderButton(); + await umbracoUi.dataType.clickConfirmRenameButton(); // Assert await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DatePicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DatePicker.spec.ts index 1eb2d0a512..3e71aa59c9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DatePicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DatePicker.spec.ts @@ -47,7 +47,7 @@ for (const datePickerType of datePickerTypes) { ]; // Act - await umbracoUi.dataType.clickOffsetTimeSlider(); + await umbracoUi.dataType.clickOffsetTimeToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Dropdown.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Dropdown.spec.ts index 2c63e953b7..5e7f3586f7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Dropdown.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Dropdown.spec.ts @@ -2,7 +2,7 @@ import {expect} from "@playwright/test"; const dataTypeName = 'Dropdown'; -let dataTypeDefaultData = null; +let dataTypeDefaultData = null; let dataTypeData = null; test.beforeEach(async ({umbracoUi, umbracoApi}) => { @@ -13,8 +13,8 @@ test.beforeEach(async ({umbracoUi, umbracoApi}) => { test.afterEach(async ({umbracoApi}) => { if (dataTypeDefaultData !== null) { - await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); - } + await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); + } }); test('can enable multiple choice', async ({umbracoApi, umbracoUi}) => { @@ -26,11 +26,11 @@ test('can enable multiple choice', async ({umbracoApi, umbracoUi}) => { // Remove all existing options dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); dataTypeData.values = []; - await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); + await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); await umbracoUi.dataType.goToDataType(dataTypeName); // Act - await umbracoUi.dataType.clickEnableMultipleChoiceSlider(); + await umbracoUi.dataType.clickEnableMultipleChoiceToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -50,7 +50,7 @@ test('can add option', async ({umbracoApi, umbracoUi}) => { // Remove all existing options dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); dataTypeData.values = []; - await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); + await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); await umbracoUi.dataType.goToDataType(dataTypeName); // Act @@ -75,7 +75,7 @@ test('can remove option', async ({umbracoApi, umbracoUi}) => { // Remove all existing options and add an option to remove dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); dataTypeData.values = removedOptionValues; - await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); + await umbracoApi.dataType.update(dataTypeData.id, dataTypeData); await umbracoUi.dataType.goToDataType(dataTypeName); // Act diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts index 6b0c4e2edd..41d7af774e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts @@ -208,7 +208,7 @@ for (const listViewType of listViewTypes) { // Act await umbracoUi.dataType.goToDataType(listViewType); - await umbracoUi.dataType.clickBulkActionPermissionsSliderByValue(bulkActionPermissionValue); + await umbracoUi.dataType.clickBulkActionPermissionsToggleByValue(bulkActionPermissionValue); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -262,7 +262,7 @@ for (const listViewType of listViewTypes) { // Act await umbracoUi.dataType.goToDataType(listViewType); - await umbracoUi.dataType.clickShowContentWorkspaceViewFirstSlider(); + await umbracoUi.dataType.clickShowContentWorkspaceViewFirstToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -280,7 +280,7 @@ for (const listViewType of listViewTypes) { // Act await umbracoUi.dataType.goToDataType(listViewType); - await umbracoUi.dataType.clickEditInInfiniteEditorSlider(); + await umbracoUi.dataType.clickEditInInfiniteEditorToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts index e29b1d2e96..d4945b1cb1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts @@ -28,7 +28,7 @@ for (const dataTypeName of dataTypes) { // Act await umbracoUi.dataType.goToDataType(dataTypeName); - await umbracoUi.dataType.clickPickMultipleItemsSlider(); + await umbracoUi.dataType.clickPickMultipleItemsToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -67,7 +67,7 @@ for (const dataTypeName of dataTypes) { // Act await umbracoUi.dataType.goToDataType(dataTypeName); - await umbracoUi.dataType.clickEnableFocalPointSlider(); + await umbracoUi.dataType.clickEnableFocalPointToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -118,7 +118,7 @@ for (const dataTypeName of dataTypes) { // Act await umbracoUi.dataType.goToDataType(dataTypeName); - await umbracoUi.dataType.clickIgnoreUserStartNodesSlider(); + await umbracoUi.dataType.clickIgnoreUserStartNodesToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MultiUrlPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MultiUrlPicker.spec.ts index 96ab56d73b..1cb557aba1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MultiUrlPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MultiUrlPicker.spec.ts @@ -14,8 +14,8 @@ test.beforeEach(async ({umbracoUi, umbracoApi}) => { test.afterEach(async ({umbracoApi}) => { if (dataTypeDefaultData !== null) { - await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); - } + await umbracoApi.dataType.update(dataTypeDefaultData.id, dataTypeDefaultData); + } }); test('can update minimum number of items value', async ({umbracoApi, umbracoUi}) => { @@ -60,7 +60,7 @@ test('can enable ignore user start nodes', async ({umbracoApi, umbracoUi}) => { }; // Act - await umbracoUi.dataType.clickIgnoreUserStartNodesSlider(); + await umbracoUi.dataType.clickIgnoreUserStartNodesToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -93,7 +93,7 @@ test('can update hide anchor/query string input', async ({umbracoApi, umbracoUi} }; // Act - await umbracoUi.dataType.clickHideAnchorQueryStringInputSlider(); + await umbracoUi.dataType.clickHideAnchorQueryStringInputToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Numeric.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Numeric.spec.ts index 77201cabd5..2bd0da1404 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Numeric.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Numeric.spec.ts @@ -77,7 +77,7 @@ test.skip('can allow decimals', async ({umbracoApi, umbracoUi}) => { }; // Act - await umbracoUi.dataType.clickAllowDecimalsSlider(); + await umbracoUi.dataType.clickAllowDecimalsToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TinyMCE.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TinyMCE.spec.ts index edc15196e7..590df30a44 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TinyMCE.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TinyMCE.spec.ts @@ -234,7 +234,7 @@ test('can enable hide label', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dataType.goToDataType(tinyMCEName); // Act - await umbracoUi.dataType.clickHideLabelSlider(); + await umbracoUi.dataType.clickHideLabelToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -278,7 +278,7 @@ test('can enable ignore user start nodes', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dataType.goToDataType(tinyMCEName); // Act - await umbracoUi.dataType.clickIgnoreUserStartNodesSlider(); + await umbracoUi.dataType.clickIgnoreUserStartNodesToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts index 2144d848d5..d999675f0e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts @@ -195,7 +195,7 @@ test('can enable ignore user start nodes', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dataType.goToDataType(tipTapName); // Act - await umbracoUi.dataType.clickIgnoreUserStartNodesSlider(); + await umbracoUi.dataType.clickIgnoreUserStartNodesToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TrueFalse.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TrueFalse.spec.ts index b465fb6ef8..3c8abcf0f2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TrueFalse.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TrueFalse.spec.ts @@ -26,7 +26,7 @@ test('can update preset value state', async ({umbracoApi, umbracoUi}) => { }; // Act - await umbracoUi.dataType.clickPresetValueSlider(); + await umbracoUi.dataType.clickPresetValueToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert @@ -42,7 +42,7 @@ test('can update show toggle labels', async ({umbracoApi, umbracoUi}) => { }; // Act - await umbracoUi.dataType.clickShowToggleLabelsSlider(); + await umbracoUi.dataType.clickShowToggleLabelsToggle(); await umbracoUi.dataType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts index ae338730d3..a5ad42852d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -39,7 +39,7 @@ test('can rename a media file', async ({umbracoApi, umbracoUi}) => { await umbracoUi.media.goToSection(ConstantHelper.sections.media); // Arrange - await umbracoUi.media.clickLabelWithName(wrongMediaFileName, true); + await umbracoUi.media.goToMediaWithName(wrongMediaFileName); await umbracoUi.media.enterMediaItemName(mediaFileName); await umbracoUi.media.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts index 61f30db521..1555986b1c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts @@ -192,7 +192,7 @@ test('can enable approved', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.member.clickMemberLinkByName(memberName); - await umbracoUi.member.clickApprovedSlider(); + await umbracoUi.member.clickApprovedToggle(); await umbracoUi.member.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RelationTypes/RelationTypes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RelationTypes/RelationTypes.spec.ts index 41e8b043eb..cd0fd93eb7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RelationTypes/RelationTypes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RelationTypes/RelationTypes.spec.ts @@ -73,7 +73,7 @@ test.skip('can update isDependency value of a relation type', async ({umbracoApi // Act await umbracoUi.relationType.openRelationTypeByNameAtRoot(relationTypeName); - await umbracoUi.relationType.clickIsDependencySlider(); + await umbracoUi.relationType.clickIsDependencyToggle(); await umbracoUi.relationType.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RenderingContent/RenderingContentWithMultipleMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RenderingContent/RenderingContentWithMultipleMediaPicker.spec.ts index 24776ed65b..0f0416066f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RenderingContent/RenderingContentWithMultipleMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RenderingContent/RenderingContentWithMultipleMediaPicker.spec.ts @@ -41,7 +41,8 @@ test('can render content with multiple media picker value', async ({umbracoApi, await umbracoUi.contentRender.doesContentRenderValueContainText(secondMediaFileName); }); -test('can render content with multiple image media picker value', async ({umbracoApi, umbracoUi}) => { +// Remove .fixme when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18531 +test.fixme('can render content with multiple image media picker value', async ({umbracoApi, umbracoUi}) => { // Arrange const dataTypeName = 'Multiple Image Media Picker'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Dashboard/Profiling.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Dashboard/Profiling.spec.ts index 61b9935557..a0d1c5e80d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Dashboard/Profiling.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Dashboard/Profiling.spec.ts @@ -8,9 +8,9 @@ test.beforeEach(async ({umbracoUi}) => { test('can update value of activate the profiler by default', async ({umbracoUi}) => { // Act - await umbracoUi.profiling.clickActivateProfilerByDefaultSlider(); + await umbracoUi.profiling.clickActivateProfilerByDefaultToggle(); await umbracoUi.reloadPage(); // Assert - await umbracoUi.profiling.isActivateProfilerByDefaultSliderChecked(true); + await umbracoUi.profiling.isActivateProfilerByDefaultToggleChecked(true); }); 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 index ff24ed2416..e04ed42622 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts @@ -360,7 +360,7 @@ test('can set is mandatory for a property in a document type', {tag: '@smoke'}, // Act await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickEditorSettingsButton(); - await umbracoUi.documentType.clickMandatorySlider(); + await umbracoUi.documentType.clickMandatoryToggle(); await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickSaveButton(); @@ -403,7 +403,7 @@ test('can allow vary by culture for a property in a document type', {tag: '@smok // Act await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickEditorSettingsButton(); - await umbracoUi.documentType.clickVaryByCultureSlider(); + await umbracoUi.documentType.clickVaryByCultureToggle(); await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickSaveButton(); 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 index dacd51f82a..2ea441b0fd 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts @@ -58,7 +58,7 @@ test('can rename a document type folder', async ({umbracoApi, umbracoUi}) => { await umbracoUi.documentType.clickActionsMenuForName(oldFolderName); await umbracoUi.documentType.clickRenameFolderButton(); await umbracoUi.documentType.enterFolderName(documentFolderName); - await umbracoUi.documentType.clickConfirmRenameFolderButton(); + await umbracoUi.documentType.clickConfirmRenameButton(); // Assert await umbracoUi.documentType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); 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 index 1a82360e12..2c307a7c2d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts @@ -83,7 +83,7 @@ test('can configure a collection for a document type', async ({umbracoApi, umbra // Act await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickStructureTab(); - await umbracoUi.documentType.clickConfigureAsACollectionButton(); + await umbracoUi.documentType.clickAddCollectionButton(); await umbracoUi.documentType.clickTextButtonWithName(collectionDataTypeName); await umbracoUi.documentType.clickSaveButton(); 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 index 3a07a0d3dd..d5bf9e3d52 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts @@ -117,7 +117,7 @@ test('can set a property as mandatory in a media type', {tag: '@smoke'}, async ( // Act await umbracoUi.mediaType.goToMediaType(mediaTypeName); await umbracoUi.mediaType.clickEditorSettingsButton(); - await umbracoUi.mediaType.clickMandatorySlider(); + await umbracoUi.mediaType.clickMandatoryToggle(); await umbracoUi.mediaType.clickSubmitButton(); await umbracoUi.mediaType.clickSaveButton(); 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 index b78d27f596..f318fd732f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts @@ -55,7 +55,7 @@ test('can rename a media type folder', async ({umbracoApi, umbracoUi}) => { await umbracoUi.mediaType.clickActionsMenuForName(oldFolderName); await umbracoUi.mediaType.clickRenameFolderButton(); await umbracoUi.mediaType.enterFolderName(mediaTypeFolderName); - await umbracoUi.mediaType.clickConfirmRenameFolderButton(); + await umbracoUi.mediaType.clickConfirmRenameButton(); // Assert await umbracoUi.mediaType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); 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 index 3ed918f9cf..920ba4f3d9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts @@ -104,7 +104,7 @@ test('can configure a collection for a media type', async ({umbracoApi, umbracoU // Act await umbracoUi.mediaType.goToMediaType(mediaTypeName); await umbracoUi.mediaType.clickStructureTab(); - await umbracoUi.mediaType.clickConfigureAsACollectionButton(); + await umbracoUi.mediaType.clickAddCollectionButton(); await umbracoUi.mediaType.clickTextButtonWithName(collectionDataTypeName); await umbracoUi.mediaType.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts index 6edddfed07..ff81a98994 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts @@ -111,7 +111,8 @@ test('can update a partial view content', {tag: '@smoke'}, async ({umbracoApi, u expect(updatedPartialView.content).toBe(updatedPartialViewContent); }); -test('can use query builder with Order By statement for a partial view', async ({umbracoApi, umbracoUi}) => { +// Remove .fixme when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18536 +test.fixme('can use query builder with Order By statement for a partial view', async ({umbracoApi, umbracoUi}) => { //Arrange const propertyAliasValue = 'UpdateDate'; const isAscending = true; @@ -150,7 +151,8 @@ test('can use query builder with Order By statement for a partial view', async ( expect(updatedPartialView.content).toBe(expectedTemplateContent); }); -test('can use query builder with Where statement for a partial view', async ({umbracoApi, umbracoUi}) => { +// Remove .fixme when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18536 +test.fixme('can use query builder with Where statement for a partial view', async ({umbracoApi, umbracoUi}) => { //Arrange const propertyAliasValue = 'Name'; const operatorValue = 'is'; diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts index cfd34fa291..0a19f29864 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Template/Templates.spec.ts @@ -175,7 +175,8 @@ test.skip('can use query builder with Order By statement for a template', async expect(templateData.content).toBe(expectedTemplateContent); }); -test('can use query builder with Where statement for a template', async ({umbracoApi, umbracoUi}) => { +// Remove .fixme when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18536 +test.fixme('can use query builder with Where statement for a template', async ({umbracoApi, umbracoUi}) => { // Arrange const propertyAliasValue = 'Name'; const operatorValue = 'is'; diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/ContentStartNodes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/ContentStartNodes.spec.ts index f59ae83870..70bd76fcca 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/ContentStartNodes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/ContentStartNodes.spec.ts @@ -69,7 +69,9 @@ test('can see parent of start node but not access it', async ({umbracoApi, umbra // Assert await umbracoUi.content.isContentInTreeVisible(rootDocumentName); await umbracoUi.content.goToContentWithName(rootDocumentName); - await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); + await umbracoUi.content.isErrorNotificationVisible(); + // TODO: Uncomment this when this issue is fixed https://github.com/umbraco/Umbraco-CMS/issues/18533 + //await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); await umbracoUi.content.clickCaretButtonForContentName(rootDocumentName); await umbracoUi.content.isChildContentInTreeVisible(rootDocumentName, childDocumentOneName); await umbracoUi.content.isChildContentInTreeVisible(rootDocumentName, childDocumentTwoName, false); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/MediaStartNodes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/MediaStartNodes.spec.ts index 81cdeb84ed..bc9637b05a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/MediaStartNodes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/User/MediaStartNodes.spec.ts @@ -63,7 +63,9 @@ test('can see parent of start node but not access it', async ({umbracoApi, umbra await umbracoUi.media.isMediaTreeItemVisible(rootFolderName); await umbracoUi.waitForTimeout(500); await umbracoUi.media.goToMediaWithName(rootFolderName); - await umbracoUi.media.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); + await umbracoUi.content.isErrorNotificationVisible(); + // TODO: Uncomment this when this issue is fixed https://github.com/umbraco/Umbraco-CMS/issues/18533 + //await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); await umbracoUi.media.clickCaretButtonForMediaName(rootFolderName); await umbracoUi.media.isChildMediaVisible(rootFolderName, childFolderOneName); await umbracoUi.media.isChildMediaVisible(rootFolderName, childFolderTwoName, false); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/ContentStartNodes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/ContentStartNodes.spec.ts index 7c7c48d186..09c8ed8e39 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/ContentStartNodes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/ContentStartNodes.spec.ts @@ -71,7 +71,9 @@ test('can see parent of start node but not access it', async ({umbracoApi, umbra // Assert await umbracoUi.content.isContentInTreeVisible(rootDocumentName); await umbracoUi.content.goToContentWithName(rootDocumentName); - await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); + await umbracoUi.content.isErrorNotificationVisible(); + // TODO: Uncomment this when this issue is fixed https://github.com/umbraco/Umbraco-CMS/issues/18533 + //await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); await umbracoUi.content.clickCaretButtonForContentName(rootDocumentName); await umbracoUi.content.isChildContentInTreeVisible(rootDocumentName, childDocumentOneName); await umbracoUi.content.isChildContentInTreeVisible(rootDocumentName, childDocumentTwoName, false); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/DefaultPermissionsInContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/DefaultPermissionsInContent.spec.ts index 12f167ec02..d3ac9aeadb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/DefaultPermissionsInContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/DefaultPermissionsInContent.spec.ts @@ -76,7 +76,9 @@ test('can not browse content node with permission disabled', async ({umbracoApi, await umbracoUi.content.goToContentWithName(rootDocumentName); // Assert - await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); + await umbracoUi.content.isErrorNotificationVisible(); + // TODO: Uncomment this when this issue is fixed https://github.com/umbraco/Umbraco-CMS/issues/18533 + //await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); }); test('can create document blueprint with permission enabled', async ({umbracoApi, umbracoUi}) => { @@ -613,6 +615,6 @@ test('can not see delete button in content for userGroup with delete permission await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); // Assert - await umbracoUi.content.isPermissionInActionsMenuVisible('Delete...', false); - await umbracoUi.content.isPermissionInActionsMenuVisible('Create...', true); + await umbracoUi.content.isPermissionInActionsMenuVisible('Delete…', false); + await umbracoUi.content.isPermissionInActionsMenuVisible('Create…', true); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/Languages.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/Languages.spec.ts index 7b535fb067..34966f2466 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/Languages.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/Languages.spec.ts @@ -62,7 +62,7 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); -test('can rename content with language set in userGroup', async ({umbracoApi, umbracoUi}) => { +test.fixme('can rename content with language set in userGroup', async ({umbracoApi, umbracoUi}) => { // Arrange const updatedContentName = 'UpdatedContentName'; userGroupId = await umbracoApi.userGroup.createUserGroupWithLanguageAndContentSection(userGroupName, englishIsoCode); @@ -75,6 +75,7 @@ test('can rename content with language set in userGroup', async ({umbracoApi, um // Act await umbracoUi.content.isDocumentReadOnly(false); await umbracoUi.content.enterContentName(updatedContentName); + // Fix this later. Currently the "Save" button changed to "Save..." button await umbracoUi.content.clickSaveButton(); await umbracoUi.content.clickSaveAndCloseButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/MediaStartNodes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/MediaStartNodes.spec.ts index b129c3979e..1a72772e3b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/MediaStartNodes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/MediaStartNodes.spec.ts @@ -64,7 +64,9 @@ test('can see parent of start node but not access it', async ({umbracoApi, umbra await umbracoUi.media.isMediaTreeItemVisible(rootFolderName); await umbracoUi.waitForTimeout(500); await umbracoUi.media.goToMediaWithName(rootFolderName); - await umbracoUi.media.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); + await umbracoUi.content.isErrorNotificationVisible(); + // TODO: Uncomment this when this issue is fixed https://github.com/umbraco/Umbraco-CMS/issues/18533 + //await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource); await umbracoUi.media.clickCaretButtonForMediaName(rootFolderName); await umbracoUi.media.isChildMediaVisible(rootFolderName, childFolderOneName); await umbracoUi.media.isChildMediaVisible(rootFolderName, childFolderTwoName, false); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts index c385b95c8d..85b103634d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -319,7 +319,7 @@ test('can allow access to all documents for a user', async ({umbracoApi, umbraco // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); - await umbracoUi.user.clickAllowAccessToAllDocumentsSlider(); + await umbracoUi.user.clickAllowAccessToAllDocumentsToggle(); await umbracoUi.user.clickSaveButton(); // Assert @@ -336,7 +336,7 @@ test('can allow access to all media for a user', async ({umbracoApi, umbracoUi}) // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); - await umbracoUi.user.clickAllowAccessToAllMediaSlider(); + await umbracoUi.user.clickAllowAccessToAllMediaToggle(); await umbracoUi.user.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs index 173f81e720..c03512b483 100644 --- a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs @@ -21,6 +21,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.HostedServices; +using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Persistence.EFCore.Locking; using Umbraco.Cms.Persistence.EFCore.Scoping; using Umbraco.Cms.Tests.Common.TestHelpers.Stubs; @@ -95,6 +96,8 @@ public static class UmbracoBuilderExtensions builder.Services.AddSingleton>(); builder.Services.AddSingleton>(); + builder.Services.AddSingleton(); + return builder; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderDisabledTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderDisabledTests.cs new file mode 100644 index 0000000000..fa9dbbeddb --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderDisabledTests.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public class + ContentTypeEditingServiceModelsBuilderDisabledTests : ContentTypeEditingServiceModelsBuilderDisabledTestsBase +{ + // test some properties from IPublishedContent + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.Id) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.Name) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.SortOrder) })] + // test some properties from IPublishedElement + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.Properties) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.ContentType) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.Key) })] + // test some methods from IPublishedContent + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.IsDraft) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.IsPublished) })] + public async Task Can_Use_Invalid_ModelsBuilder_PropertyType_Alias_When_ModelsBuilderIsDisabled( + string propertyTypeAlias) + { + var propertyType = ContentTypePropertyTypeModel("Test Property", propertyTypeAlias); + var createModel = ContentTypeCreateModel("Test", propertyTypes: new[] { propertyType }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.Success, result.Status); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderDisabledTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderDisabledTestsBase.cs new file mode 100644 index 0000000000..55e632ba8a --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderDisabledTestsBase.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Infrastructure.ModelsBuilder.Options; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +/// +/// Unlike this testbase does not configure the modelsbuilder based +/// which has the same effect as disabling it completely as only loads in that part anyway. +/// +public class ContentTypeEditingServiceModelsBuilderDisabledTestsBase : ContentTypeEditingServiceTestsBase +{ +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderEnabledTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderEnabledTests.cs new file mode 100644 index 0000000000..88be628734 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderEnabledTests.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public class ContentTypeEditingServiceModelsBuilderEnabledTests : ContentTypeEditingServiceModelsBuilderEnabledTestsBase +{ + // test some properties from IPublishedContent + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.Id) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.Name) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.SortOrder) })] + // test some properties from IPublishedElement + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.Properties) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.ContentType) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.Key) })] + // test some methods from IPublishedContent + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.IsDraft) })] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { nameof(IPublishedContent.IsPublished) })] + public async Task Cannot_Use_Invalid_ModelsBuilder_PropertyType_Alias_When_ModelsBuilderIsEnabled( + string propertyTypeAlias) + { + var propertyType = ContentTypePropertyTypeModel("Test Property", propertyTypeAlias); + var createModel = ContentTypeCreateModel("Test", propertyTypes: new[] { propertyType }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidPropertyTypeAlias, result.Status); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderEnabledTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderEnabledTestsBase.cs new file mode 100644 index 0000000000..5709bec3c7 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceModelsBuilderEnabledTestsBase.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Infrastructure.ModelsBuilder.Options; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public class ContentTypeEditingServiceModelsBuilderEnabledTestsBase : ContentTypeEditingServiceTestsBase +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.ConfigureOptions(); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs index da23e87857..bac636136f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs @@ -708,17 +708,6 @@ public partial class ContentTypeEditingServiceTests Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status); } - // test some properties from IPublishedContent - [TestCase(nameof(IPublishedContent.Id))] - [TestCase(nameof(IPublishedContent.Name))] - [TestCase(nameof(IPublishedContent.SortOrder))] - // test some properties from IPublishedElement - [TestCase(nameof(IPublishedElement.Properties))] - [TestCase(nameof(IPublishedElement.ContentType))] - [TestCase(nameof(IPublishedElement.Key))] - // test some methods from IPublishedContent - [TestCase(nameof(IPublishedContent.IsDraft))] - [TestCase(nameof(IPublishedContent.IsPublished))] [TestCase("")] [TestCase(" ")] [TestCase(" ")] @@ -727,21 +716,12 @@ public partial class ContentTypeEditingServiceTests [TestCase("!\"#¤%&/()=)?`")] public async Task Cannot_Use_Invalid_PropertyType_Alias(string propertyTypeAlias) { - // ensure that property casing is ignored when handling reserved property aliases - var propertyTypeAliases = new[] - { - propertyTypeAlias, propertyTypeAlias.ToLowerInvariant(), propertyTypeAlias.ToUpperInvariant() - }; + var propertyType = ContentTypePropertyTypeModel("Test Property", propertyTypeAlias); + var createModel = ContentTypeCreateModel("Test", propertyTypes: new[] { propertyType }); - foreach (var alias in propertyTypeAliases) - { - var propertyType = ContentTypePropertyTypeModel("Test Property", alias); - var createModel = ContentTypeCreateModel("Test", propertyTypes: new[] { propertyType }); - - var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); - Assert.IsFalse(result.Success); - Assert.AreEqual(ContentTypeOperationStatus.InvalidPropertyTypeAlias, result.Status); - } + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidPropertyTypeAlias, result.Status); } [TestCase("testProperty", "testProperty")] @@ -816,9 +796,7 @@ public partial class ContentTypeEditingServiceTests [TestCase(".")] [TestCase("-")] [TestCase("!\"#¤%&/()=)?`")] - [TestCase("system")] - [TestCase("System")] - [TestCase("SYSTEM")] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { "System"})] public async Task Cannot_Use_Invalid_Alias(string contentTypeAlias) { var createModel = ContentTypeCreateModel("Test", contentTypeAlias); @@ -1095,4 +1073,32 @@ public partial class ContentTypeEditingServiceTests Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidContainerType, result.Status); } + + [TestCase(false, true)] + [TestCase(true, false)] + public async Task Cannot_Have_Element_Type_Mismatched_Inheritance(bool parentIsElement, bool childIsElement) + { + var parentModel = ContentTypeCreateModel("Parent1", isElement: parentIsElement); + + var parentKey = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result?.Key; + Assert.IsTrue(parentKey.HasValue); + + Composition[] composition = + { + new() + { + CompositionType = CompositionType.Inheritance, Key = parentKey.Value, + } + }; + + var childModel = ContentTypeCreateModel( + "Child", + compositions: composition, + isElement: childIsElement); + + var result = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidElementFlagComparedToParent, result.Status); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTestsBase.cs index 1068f4b9a1..f478904684 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTestsBase.cs @@ -180,4 +180,11 @@ public abstract class ContentTypeEditingServiceTestsBase : UmbracoIntegrationTes Type = type, Key = key ?? Guid.NewGuid(), }; + + protected static IEnumerable DifferentCapitalizedAlias(string baseAlias) + { + yield return baseAlias; + yield return baseAlias.ToLowerInvariant(); + yield return baseAlias.ToUpperInvariant(); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs index 7e1d1f163f..3d419ff955 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs @@ -18,7 +18,6 @@ using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; -using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.SyntaxProvider; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs index a9ff943921..ef8b8733c7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs @@ -632,6 +632,7 @@ public class VariationTests var dataValueEditorFactory = Mock.Of(x => x.Create(It.IsAny()) == new TextOnlyValueEditor( attribute, + Mock.Of(), Mock.Of(), new SystemTextJsonSerializer(), Mock.Of())); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs new file mode 100644 index 0000000000..9edc858532 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs @@ -0,0 +1,165 @@ +using System.Globalization; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.Serialization; +using static Umbraco.Cms.Core.PropertyEditors.BlockListPropertyEditorBase; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class BlockListEditorPropertyValueEditorTests +{ + [Test] + public void Validates_Null_As_Below_Configured_Min() + { + var editor = CreateValueEditor(); + var result = editor.Validate(null, false, null, PropertyValidationContext.Empty()); + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual($"validation_entriesShort", validationResult.ErrorMessage); + } + + [TestCase(0, false)] + [TestCase(1, false)] + [TestCase(2, true)] + [TestCase(3, true)] + public void Validates_Number_Of_Items_Is_Greater_Than_Or_Equal_To_Configured_Min(int numberOfBlocks, bool expectedSuccess) + { + var value = CreateBlocksJson(numberOfBlocks); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_entriesShort", validationResult.ErrorMessage); + } + } + + [TestCase(3, true)] + [TestCase(4, true)] + [TestCase(5, false)] + public void Validates_Number_Of_Items_Is_Less_Than_Or_Equal_To_Configured_Max(int numberOfBlocks, bool expectedSuccess) + { + var value = CreateBlocksJson(numberOfBlocks); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_entriesExceed", validationResult.ErrorMessage); + } + } + + private static JsonObject CreateBlocksJson(int numberOfBlocks) + { + var layoutItems = new JsonArray(); + var contentData = new JsonArray(); + for (int i = 0; i < numberOfBlocks; i++) + { + layoutItems.Add(CreateLayoutBlockJson()); + contentData.Add(CreateContentDataBlockJson()); + } + + return new JsonObject + { + { + "layout", new JsonObject + { + { "Umbraco.BlockList", layoutItems }, + } + }, + { "contentData", contentData }, + }; + } + + private static JsonObject CreateLayoutBlockJson() => + new() + { + { "$type", "BlockListLayoutItem" }, + { "contentKey", Guid.NewGuid() }, + }; + + private static JsonObject CreateContentDataBlockJson() => + new() + { + { "contentTypeKey", Guid.Parse("01935a73-c86b-4521-9dcb-ad7cea402215") }, + { "key", Guid.NewGuid() }, + { + "values", + new JsonArray + { + new JsonObject + { + { "editorAlias", "Umbraco.TextBox" }, + { "alias", "message" }, + { "value", "Hello" }, + }, + } + } + }; + + private static BlockListEditorPropertyValueEditor CreateValueEditor() + { + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + + var jsonSerializer = new SystemTextJsonSerializer(); + var languageService = Mock.Of(); + + return new BlockListEditorPropertyValueEditor( + new DataEditorAttribute("alias"), + new BlockListEditorDataConverter(jsonSerializer), + new(new DataEditorCollection(() => [])), + new DataValueReferenceFactoryCollection(Enumerable.Empty), + Mock.Of(), + Mock.Of(), + localizedTextServiceMock.Object, + new NullLogger(), + Mock.Of(), + jsonSerializer, + Mock.Of(), + new BlockEditorVarianceHandler(languageService, Mock.Of()), + languageService, + Mock.Of()) + { + ConfigurationObject = new BlockListConfiguration + { + ValidationLimit = new BlockListConfiguration.NumberRange + { + Min = 2, + Max = 4 + }, + }, + }; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerPropertyValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerPropertyValueEditorTests.cs new file mode 100644 index 0000000000..17b49f6b13 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorPickerPropertyValueEditorTests.cs @@ -0,0 +1,63 @@ +using System.Globalization; +using Humanizer; +using System.Text.Json.Nodes; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class ColorPickerPropertyValueEditorTests +{ + [TestCase("#ffffff", true)] + [TestCase("#f0f0f0", false)] + public void Validates_Is_Configured_Color(string color, bool expectedSuccess) + { + var value = JsonNode.Parse($"{{\"label\": \"\", \"value\": \"{color}\"}}"); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_invalidColor"); + } + } + + private static ColorPickerPropertyEditor.ColorPickerPropertyValueEditor CreateValueEditor() + { + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + return new ColorPickerPropertyEditor.ColorPickerPropertyValueEditor( + Mock.Of(), + Mock.Of(), + Mock.Of(), + new DataEditorAttribute("alias"), + localizedTextServiceMock.Object) + { + ConfigurationObject = new ColorPickerConfiguration + { + Items = [ + new ColorPickerConfiguration.ColorPickerItem { Value = "ffffff", Label = "White" }, + new ColorPickerConfiguration.ColorPickerItem { Value = "000000", Label = "Black" } + ] + } + }; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 217b820c78..16ea5c11f9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -29,6 +29,7 @@ public class DataValueEditorReuseTests .Setup(m => m.Create(It.IsAny())) .Returns(() => new TextOnlyValueEditor( new DataEditorAttribute("a"), + Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of())); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalPropertyValueEditorTests.cs similarity index 70% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalPropertyValueEditorTests.cs index 59569c7583..92b4f8579a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalPropertyValueEditorTests.cs @@ -14,7 +14,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class DecimalValueEditorTests +public class DecimalPropertyValueEditorTests { // annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :( private Dictionary _valuesAndExpectedResults = new(); @@ -88,7 +88,7 @@ public class DecimalValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, $"The value {value} is not a valid decimal"); + Assert.AreEqual($"The value {value} is not a valid decimal", validationResult.ErrorMessage); } } @@ -108,7 +108,7 @@ public class DecimalValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMinimum"); + Assert.AreEqual("validation_outOfRangeMinimum", validationResult.ErrorMessage); } } @@ -128,15 +128,15 @@ public class DecimalValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMaximum"); + Assert.AreEqual("validation_outOfRangeMaximum", validationResult.ErrorMessage); } } - [TestCase(1.4, false)] - [TestCase(1.5, true)] - public void Validates_Matches_Configured_Step(object value, bool expectedSuccess) + [TestCase(1.8, true)] + [TestCase(2.2, false)] + public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max_With_Configured_Whole_Numbers(object value, bool expectedSuccess) { - var editor = CreateValueEditor(); + var editor = CreateValueEditor(min: 1, max: 2); var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); if (expectedSuccess) { @@ -147,7 +147,27 @@ public class DecimalValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, "validation_invalidStep"); + Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMaximum"); + } + } + + [TestCase(0.2, 1.4, false)] + [TestCase(0.2, 1.5, true)] + [TestCase(0.0, 1.4, true)] // A step of zero would trigger a divide by zero error in evaluating. So we always pass validation for zero, as effectively any step value is valid. + public void Validates_Matches_Configured_Step(double step, object value, bool expectedSuccess) + { + var editor = CreateValueEditor(step: step); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_invalidStep", validationResult.ErrorMessage); } } @@ -164,7 +184,7 @@ public class DecimalValueEditorTests return CreateValueEditor().ToEditor(property.Object); } - private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEditor() + private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEditor(double min = 1.1, double max = 1.9, double step = 0.2) { var localizedTextServiceMock = new Mock(); localizedTextServiceMock.Setup(x => x.Localize( @@ -173,6 +193,37 @@ public class DecimalValueEditorTests It.IsAny(), It.IsAny>())) .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + + // When configuration is populated from the deserialized JSON, whole number values are deserialized as integers. + // So we want to replicate that in our tests. + var configuration = new Dictionary(); + if (min % 1 == 0) + { + configuration.Add("min", (int)min); + } + else + { + configuration.Add("min", min); + } + + if (max % 1 == 0) + { + configuration.Add("max", (int)max); + } + else + { + configuration.Add("max", max); + } + + if (step % 1 == 0) + { + configuration.Add("step", (int)step); + } + else + { + configuration.Add("step", step); + } + return new DecimalPropertyEditor.DecimalPropertyValueEditor( Mock.Of(), Mock.Of(), @@ -180,12 +231,7 @@ public class DecimalValueEditorTests new DataEditorAttribute("alias"), localizedTextServiceMock.Object) { - ConfigurationObject = new Dictionary - { - { "min", 1.1 }, - { "max", 1.9 }, - { "step", 0.2 } - } + ConfigurationObject = configuration }; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerPropertyValueEditorTests.cs similarity index 92% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerPropertyValueEditorTests.cs index d9545b8126..a0c0a38ab3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerPropertyValueEditorTests.cs @@ -14,7 +14,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class IntegerValueEditorTests +public class IntegerPropertyValueEditorTests { // annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :( private Dictionary _valuesAndExpectedResults = new(); @@ -132,11 +132,12 @@ public class IntegerValueEditorTests } } - [TestCase(17, false)] - [TestCase(18, true)] - public void Validates_Matches_Configured_Step(object value, bool expectedSuccess) + [TestCase(2, 17, false)] + [TestCase(2, 18, true)] + [TestCase(0, 17, true)] // A step of zero would trigger a divide by zero error in evaluating. So we always pass validation for zero, as effectively any step value is valid. + public void Validates_Matches_Configured_Step(int step, object value, bool expectedSuccess) { - var editor = CreateValueEditor(); + var editor = CreateValueEditor(step: step); var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); if (expectedSuccess) { @@ -164,7 +165,7 @@ public class IntegerValueEditorTests return CreateValueEditor().ToEditor(property.Object); } - private static IntegerPropertyEditor.IntegerPropertyValueEditor CreateValueEditor() + private static IntegerPropertyEditor.IntegerPropertyValueEditor CreateValueEditor(int step = 2) { var localizedTextServiceMock = new Mock(); localizedTextServiceMock.Setup(x => x.Localize( @@ -184,7 +185,7 @@ public class IntegerValueEditorTests { { "min", 10 }, { "max", 20 }, - { "step", 2 } + { "step", step } } }; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs index 4f6ee99ad4..ad82ce3179 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs @@ -22,7 +22,7 @@ internal class MediaPicker3ValueEditorValidationTests [TestCase(false, false)] public void Validates_Start_Node_Immediate_Parent(bool shouldSucceed, bool hasValidParentKey) { - var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + var (valueEditor, mediaTypeServiceMock, _, mediaNavigationQueryServiceMock) = CreateValueEditor(); Guid? validParentKey = Guid.NewGuid(); var mediaKey = Guid.NewGuid(); @@ -49,7 +49,7 @@ internal class MediaPicker3ValueEditorValidationTests [Test] public void Validates_Start_Node_Parent_Not_Found() { - var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + var (valueEditor, mediaTypeServiceMock, _, mediaNavigationQueryServiceMock) = CreateValueEditor(); Guid? parentKey = null; var mediaKey = Guid.NewGuid(); @@ -71,7 +71,7 @@ internal class MediaPicker3ValueEditorValidationTests [TestCase(false, true, false)] public void Validates_Start_Node_Ancestor(bool shouldSucceed, bool findsAncestor, bool hasValidAncestorKey) { - var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + var (valueEditor, mediaTypeServiceMock, _, mediaNavigationQueryServiceMock) = CreateValueEditor(); Guid ancestorKey = Guid.NewGuid(); Guid? parentKey = Guid.NewGuid(); @@ -90,26 +90,32 @@ internal class MediaPicker3ValueEditorValidationTests ValidateResult(shouldSucceed, result); } - [TestCase(true, true, true)] - [TestCase(false, true, false)] - [TestCase(false, false, true)] - public void Validates_Allowed_Type(bool shouldSucceed, bool hasAllowedType, bool findsMediaType) + [TestCase(true, true, true, false)] + [TestCase(false, true, false, false)] + [TestCase(false, false, true, false)] + [TestCase(true, true, true, true)] + [TestCase(false, true, false, true)] + [TestCase(false, false, true, true)] + public void Validates_Allowed_Type(bool shouldSucceed, bool hasAllowedType, bool findsMediaType, bool valueProvidesMediaTypeAlias) { - var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + var (valueEditor, mediaTypeServiceMock, mediaServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); var mediaKey = Guid.NewGuid(); var mediaTypeKey = Guid.NewGuid(); var mediaTypeAlias = "Alias"; valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Filter = $"{mediaTypeKey}" }; var mediaTypeMock = new Mock(); + var mediaMock = new Mock(); if (hasAllowedType) { mediaTypeMock.Setup(x => x.Key).Returns(mediaTypeKey); + mediaMock.SetupGet(x => x.ContentType.Alias).Returns(mediaTypeAlias); } else { mediaTypeMock.Setup(x => x.Key).Returns(Guid.NewGuid()); + mediaMock.SetupGet(x => x.ContentType.Alias).Returns("AnotherAlias"); } if (findsMediaType) @@ -121,7 +127,13 @@ internal class MediaPicker3ValueEditorValidationTests mediaTypeServiceMock.Setup(x => x.Get(It.IsAny())).Returns((IMediaType)null); } - var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"" + mediaTypeAlias + "\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]"; + if (valueProvidesMediaTypeAlias is false) + { + mediaServiceMock.Setup(x => x.GetByIds(It.Is>(y => y.First() == mediaKey))).Returns([mediaMock.Object]); + } + + var providedMediaTypeAlias = valueProvidesMediaTypeAlias ? mediaTypeAlias : string.Empty; + var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"" + providedMediaTypeAlias + "\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]"; var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); ValidateResult(shouldSucceed, result); @@ -134,7 +146,7 @@ internal class MediaPicker3ValueEditorValidationTests [TestCase("[]", false, true)] public void Validates_Multiple(string value, bool multiple, bool succeed) { - var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + var (valueEditor, mediaTypeServiceMock, _, mediaNavigationQueryServiceMock) = CreateValueEditor(); valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = multiple }; @@ -150,7 +162,7 @@ internal class MediaPicker3ValueEditorValidationTests [TestCase("[]", 0, true)] public void Validates_Min_Limit(string value, int min, bool succeed) { - var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + var (valueEditor, mediaTypeServiceMock, _, mediaNavigationQueryServiceMock) = CreateValueEditor(); valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = true, ValidationLimit = new MediaPicker3Configuration.NumberRange { Min = min } }; @@ -168,7 +180,7 @@ internal class MediaPicker3ValueEditorValidationTests [TestCase("[]", 0, true)] public void Validates_Max_Limit(string value, int max, bool succeed) { - var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + var (valueEditor, mediaTypeServiceMock, _, mediaNavigationQueryServiceMock) = CreateValueEditor(); valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = true, ValidationLimit = new MediaPicker3Configuration.NumberRange { Max = max } }; @@ -188,9 +200,10 @@ internal class MediaPicker3ValueEditorValidationTests } } - private static (MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor ValueEditor, Mock MediaTypeServiceMock, Mock MediaNavigationQueryServiceMock) CreateValueEditor() + private static (MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor ValueEditor, Mock MediaTypeServiceMock, Mock MediaServiceMock, Mock MediaNavigationQueryServiceMock) CreateValueEditor() { var mediaTypeServiceMock = new Mock(); + var mediaServiceMock = new Mock(); var mediaNavigationQueryServiceMock = new Mock(); var valueEditor = new MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor( Mock.Of(), @@ -198,7 +211,7 @@ internal class MediaPicker3ValueEditorValidationTests Mock.Of(), new DataEditorAttribute("alias"), Mock.Of(), - Mock.Of(), + mediaServiceMock.Object, Mock.Of(), Mock.Of(), Mock.Of(), @@ -210,6 +223,6 @@ internal class MediaPicker3ValueEditorValidationTests ConfigurationObject = new MediaPicker3Configuration() }; - return (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock); + return (valueEditor, mediaTypeServiceMock, mediaServiceMock, mediaNavigationQueryServiceMock); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiNodeTreePickerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiNodeTreePickerTests.cs index 1f84fb7400..f381e845ec 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiNodeTreePickerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiNodeTreePickerTests.cs @@ -1,13 +1,16 @@ -using System.Text.Json.Nodes; +using System.Data; +using System.Text.Json.Nodes; using Moq; using NUnit.Framework; -using Org.BouncyCastle.Asn1.X500; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Serialization; @@ -244,11 +247,30 @@ public class MultiNodeTreePickerTests private static MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor CreateValueEditor( IJsonSerializer? jsonSerializer = null) { + var mockScope = new Mock(); + var mockScopeProvider = new Mock(); + mockScopeProvider + .Setup(x => x.CreateCoreScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockScope.Object); + var valueEditor = new MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor( Mock.Of(), jsonSerializer ?? Mock.Of(), Mock.Of(), - new DataEditorAttribute("alias")); + new DataEditorAttribute("alias"), + Mock.Of(), + Mock.Of(), + mockScopeProvider.Object, + Mock.Of(), + Mock.Of(), + Mock.Of()); return valueEditor; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiNodeTreePickerValidationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiNodeTreePickerValidationTests.cs new file mode 100644 index 0000000000..d9d4fda2c5 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiNodeTreePickerValidationTests.cs @@ -0,0 +1,217 @@ +using System.ComponentModel.DataAnnotations; +using System.Data; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class MultiNodeTreePickerValidationTests +{ + // Remember 0 = no limit + [TestCase(0, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")] + [TestCase(1, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")] + [TestCase(2, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"},{\"type\":\"document\",\"unique\":\"25ef6fd2-db48-450a-8c48-df3ad75adf4b\"}]")] + [TestCase(3, false, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"},{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")] + [TestCase(2, false, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")] + [TestCase(1, false, null)] + [TestCase(0, true, null)] + public void Validates_Minimum_Entries(int min, bool shouldSucceed, string? value) + { + var (valueEditor, _, _, _, _) = CreateValueEditor(); + valueEditor.ConfigurationObject = new MultiNodePickerConfiguration { MinNumber = min}; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + + TestShouldSucceed(shouldSucceed, result); + } + + private static void TestShouldSucceed(bool shouldSucceed, IEnumerable result) + { + if (shouldSucceed) + { + Assert.IsEmpty(result); + } + else + { + Assert.IsNotEmpty(result); + } + } + + [TestCase(0, true, "[]")] + [TestCase(1, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")] + [TestCase(0, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")] + [TestCase(1, false, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"},{\"type\":\"document\",\"unique\":\"25ef6fd2-db48-450a-8c48-df3ad75adf4b\"}]")] + [TestCase(3, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"},{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")] + [TestCase(2, true, "[{\"type\":\"document\",\"unique\":\"86eb02a7-793f-4406-9152-9736b6b64bee\"}]")] + public void Validates_Maximum_Entries(int max, bool shouldSucceed, string value) + { + var (valueEditor, _, _, _, _) = CreateValueEditor(); + valueEditor.ConfigurationObject = new MultiNodePickerConfiguration { MaxNumber = max }; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + + TestShouldSucceed(shouldSucceed, result); + } + + private readonly Dictionary _entityTypeMap = new() + { + { Constants.ObjectTypes.Document, Guid.Parse("08035A7E-AE9C-4D36-BA2E-63F639005758") }, + { Constants.ObjectTypes.Media, Guid.Parse("AAF97C7D-A586-45CC-AC7F-CE0A80BCFEE3") }, + { Constants.ObjectTypes.Member, Guid.Parse("E477804E-C903-470B-B7EC-67DCAF71E37C") }, + }; + + private class ObjectTypeTestSetup + { + public ObjectTypeTestSetup(string expectedObjectType, bool shouldSucceed, string value) + { + ExpectedObjectType = expectedObjectType; + ShouldSucceed = shouldSucceed; + Value = value; + } + + public string ExpectedObjectType { get; } + + public bool ShouldSucceed { get; } + + public string Value { get; } + } + + private void SetupEntityServiceForObjectTypeTest(Mock entityServiceMock) + { + foreach (var objectTypeEntity in _entityTypeMap) + { + var entity = new Mock(); + entity.Setup(x => x.NodeObjectType).Returns(objectTypeEntity.Key); + entityServiceMock.Setup(x => x.Get(objectTypeEntity.Value)).Returns(entity.Object); + } + } + + private IEnumerable GetObjectTypeTestSetup() => + [ + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, true, "[]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, true, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Media]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Member]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}, {{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Media]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Member]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Document]}\"}}]"), + new(MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType, false, $"[{{\"type\":\"document\",\"unique\":\"{_entityTypeMap[Constants.ObjectTypes.Media]}\"}}]"), + ]; + + [Test] + public void Validates_Object_Type() + { + var setups = GetObjectTypeTestSetup(); + + foreach (var setup in setups) + { + var (valueEditor, entityServiceMock, _, _, _) = CreateValueEditor(); + SetupEntityServiceForObjectTypeTest(entityServiceMock); + valueEditor.ConfigurationObject = new MultiNodePickerConfiguration { TreeSource = new MultiNodePickerConfigurationTreeSource() { ObjectType = setup.ExpectedObjectType } }; + var result = valueEditor.Validate(setup.Value, false, null, PropertyValidationContext.Empty()); + + TestShouldSucceed(setup.ShouldSucceed, result); + } + } + + [TestCase(true, true, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType)] + [TestCase(true, true, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType)] + [TestCase(true, true, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType)] + [TestCase(false, false, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType)] + [TestCase(false, false, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType)] + [TestCase(false, false, true, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType)] + [TestCase(false, true, false, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.DocumentObjectType)] + [TestCase(false, true, false, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MediaObjectType)] + [TestCase(false, true, false, MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor.MemberObjectType)] + public void Validates_Allowed_Type(bool shouldSucceed, bool hasAllowedType, bool findsContent, string objectType) + { + var (valueEditor, _, contentService, mediaService, memberService) = CreateValueEditor(); + + var expectedEntityKey = Guid.NewGuid(); + var allowedTypeKey = Guid.NewGuid(); + valueEditor.ConfigurationObject = new MultiNodePickerConfiguration() + { + Filter = $"{allowedTypeKey}", + TreeSource = new MultiNodePickerConfigurationTreeSource { ObjectType = objectType }, + }; + + var contentTypeMock = new Mock(); + contentTypeMock.Setup(x => x.Key).Returns(() => hasAllowedType ? allowedTypeKey : Guid.NewGuid()); + + var contentMock = new Mock(); + contentMock.Setup(x => x.ContentType).Returns(contentTypeMock.Object); + contentService.Setup(x => x.GetById(expectedEntityKey)).Returns(contentMock.Object); + + var mediaMock = new Mock(); + mediaMock.Setup(x => x.ContentType).Returns(contentTypeMock.Object); + mediaService.Setup(x => x.GetById(expectedEntityKey)).Returns(mediaMock.Object); + + var memberMock = new Mock(); + memberMock.Setup(x => x.ContentType).Returns(contentTypeMock.Object); + memberService.Setup(x => x.GetById(expectedEntityKey)).Returns(memberMock.Object); + + var actualkey = findsContent ? expectedEntityKey : Guid.NewGuid(); + var value = $"[{{\"type\":\"document\",\"unique\":\"{actualkey}\"}}]"; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + TestShouldSucceed(shouldSucceed, result); + + } + + private static (MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor ValueEditor, + Mock EntityService, + Mock ContentService, + Mock MediaService, + Mock MemberService) CreateValueEditor() + { + var entityServiceMock = new Mock(); + var contentServiceMock = new Mock(); + var mediaServiceMock = new Mock(); + var memberServiceMock = new Mock(); + + var mockScope = new Mock(); + var mockScopeProvider = new Mock(); + mockScopeProvider + .Setup(x => x.CreateCoreScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockScope.Object); + + var valueEditor = new MultiNodeTreePickerPropertyEditor.MultiNodeTreePickerPropertyValueEditor( + Mock.Of(), + new SystemTextJsonSerializer(), + Mock.Of(), + new DataEditorAttribute("alias"), + Mock.Of(), + entityServiceMock.Object, + mockScopeProvider.Object, + contentServiceMock.Object, + mediaServiceMock.Object, + memberServiceMock.Object) + { + ConfigurationObject = new MultiNodePickerConfiguration(), + }; + + return (valueEditor, entityServiceMock, contentServiceMock, mediaServiceMock, memberServiceMock); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs index 5b76313c2a..1075510c91 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs @@ -1,11 +1,13 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Globalization; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -19,31 +21,22 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; /// multiple values such as the drop down list, check box list, color picker, etc.... /// /// -/// Mostly this used to test the we'd store INT Ids in the Db but publish STRING values or sometimes the INT values +/// Some of these tests are to verify that the we'd store INT Ids in the Db but publish STRING values or sometimes the INT values /// to cache. Now we always just deal with strings and we'll keep the tests that show that. /// [TestFixture] public class MultiValuePropertyEditorTests { [Test] - public void DropDownMultipleValueEditor_Format_Data_For_Cache() + public void MultipleValueEditor_WithMultipleValues_Format_Data_For_Cache() { var dataValueEditorFactoryMock = new Mock(); var serializer = new SystemTextConfigurationEditorJsonSerializer(); var checkBoxListPropertyEditor = new CheckBoxListPropertyEditor( dataValueEditorFactoryMock.Object, - Mock.Of(), serializer); - var dataType = new DataType(checkBoxListPropertyEditor, serializer) - { - Id = 1, - }; - dataType.ConfigurationData = dataType.Editor!.GetConfigurationEditor() - .FromConfigurationObject( - new ValueListConfiguration - { - Items = ["Value 1", "Value 2", "Value 3"] - }, - serializer); + Mock.Of(), + serializer); + var dataType = CreateAndConfigureDataType(serializer, checkBoxListPropertyEditor); var configuration = dataType.ConfigurationObject as ValueListConfiguration; Assert.NotNull(configuration); @@ -54,13 +47,7 @@ public class MultiValuePropertyEditorTests .Setup(x => x.GetDataType(It.IsAny())) .Returns(dataType); - // TODO use builders instead of this mess - var multipleValueEditor = new MultipleValueEditor( - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox)); + var multipleValueEditor = CreateValueEditor(); dataValueEditorFactoryMock .Setup(x => x.Create(It.IsAny())) .Returns(multipleValueEditor); @@ -76,25 +63,15 @@ public class MultiValuePropertyEditorTests } [Test] - public void DropDownValueEditor_Format_Data_For_Cache() + public void MultipleValueEditor_WithSingleValue_Format_Data_For_Cache() { var dataValueEditorFactoryMock = new Mock(); - var serializer = new SystemTextConfigurationEditorJsonSerializer(); var checkBoxListPropertyEditor = new CheckBoxListPropertyEditor( dataValueEditorFactoryMock.Object, - Mock.Of(), serializer); - var dataType = new DataType(checkBoxListPropertyEditor, serializer) - { - Id = 1, - }; - dataType.ConfigurationData = dataType.Editor!.GetConfigurationEditor() - .FromConfigurationObject( - new ValueListConfiguration - { - Items = ["Value 1", "Value 2", "Value 3"] - }, - serializer); + Mock.Of(), + serializer); + var dataType = CreateAndConfigureDataType(serializer, checkBoxListPropertyEditor); var configuration = dataType.ConfigurationObject as ValueListConfiguration; Assert.NotNull(configuration); @@ -105,13 +82,7 @@ public class MultiValuePropertyEditorTests .Setup(x => x.GetDataType(It.IsAny())) .Returns(dataType); - // TODO use builders instead of this mess - var multipleValueEditor = new MultipleValueEditor( - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox)); + var multipleValueEditor = CreateValueEditor(); dataValueEditorFactoryMock .Setup(x => x.Create(It.IsAny())) .Returns(multipleValueEditor); @@ -125,14 +96,15 @@ public class MultiValuePropertyEditorTests } [Test] - public void DropDownPreValueEditor_Format_Data_For_Editor() + public void MultipleValueEditor_Format_Data_For_Editor() { var dataValueEditorFactoryMock = new Mock(); var serializer = new SystemTextConfigurationEditorJsonSerializer(); var checkBoxListPropertyEditor = new CheckBoxListPropertyEditor( dataValueEditorFactoryMock.Object, - Mock.Of(), serializer); + Mock.Of(), + serializer); var dataType = new DataType(checkBoxListPropertyEditor, serializer) { Id = 1, @@ -155,4 +127,62 @@ public class MultiValuePropertyEditorTests Assert.AreEqual("Item 2", result.Items[1]); Assert.AreEqual("Item 3", result.Items[2]); } + + [TestCase("Red", true, "")] + [TestCase("Yellow", false, "notOneOfOptions")] + [TestCase("Red,Green", true, "")] + [TestCase("Red,Yellow,Purple", false, "multipleNotOneOfOptions")] + public void MultipleValueEditor_Validates_Single_Value(string values, bool expectedSuccess, string expectedValidationMessageKey) + { + var editor = CreateValueEditor(); + editor.ConfigurationObject = new ValueListConfiguration + { + Items = ["Red", "Green", "Blue"], + }; + var result = editor.Validate(values.Split(','), false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual($"validation_{expectedValidationMessageKey}", validationResult.ErrorMessage); + } + } + + private static MultipleValueEditor CreateValueEditor() + { + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + return new( + localizedTextServiceMock.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + new DataEditorAttribute(Constants.PropertyEditors.Aliases.CheckBoxList)); + } + + private static DataType CreateAndConfigureDataType(SystemTextConfigurationEditorJsonSerializer serializer, CheckBoxListPropertyEditor checkBoxListPropertyEditor) + { + var dataType = new DataType(checkBoxListPropertyEditor, serializer) + { + Id = 1, + }; + dataType.ConfigurationData = dataType.Editor!.GetConfigurationEditor() + .FromConfigurationObject( + new ValueListConfiguration + { + Items = ["Value 1", "Value 2", "Value 3"] + }, + serializer); + return dataType; + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringPropertyValueEditorTests.cs similarity index 55% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringPropertyValueEditorTests.cs index 94452a2ec4..c3a814075a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultipleTextStringPropertyValueEditorTests.cs @@ -1,9 +1,12 @@ -using Moq; +using System.Globalization; +using System.Text.Json.Nodes; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -12,7 +15,7 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class MultipleTextStringValueEditorTests +public class MultipleTextStringPropertyValueEditorTests { [Test] public void Can_Handle_Invalid_Values_From_Editor() @@ -114,6 +117,71 @@ public class MultipleTextStringValueEditorTests Assert.IsEmpty(result); } + [Test] + public void Validates_Null_As_Below_Configured_Min() + { + var editor = CreateValueEditor(); + var result = editor.Validate(null, false, null, PropertyValidationContext.Empty()); + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual($"validation_outOfRangeMultipleItemsMinimum", validationResult.ErrorMessage); + } + + [TestCase(0, false, "outOfRangeMultipleItemsMinimum")] + [TestCase(1, false, "outOfRangeSingleItemMinimum")] + [TestCase(2, true, "")] + [TestCase(3, true, "")] + public void Validates_Number_Of_Items_Is_Greater_Than_Or_Equal_To_Configured_Min(int numberOfStrings, bool expectedSuccess, string expectedValidationMessageKey) + { + var value = Enumerable.Range(1, numberOfStrings).Select(x => x.ToString()); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual($"validation_{expectedValidationMessageKey}", validationResult.ErrorMessage); + } + } + + [TestCase(3, true)] + [TestCase(4, true)] + [TestCase(5, false)] + public void Validates_Number_Of_Items_Is_Less_Than_Or_Equal_To_Configured_Max(int numberOfStrings, bool expectedSuccess) + { + var value = Enumerable.Range(1, numberOfStrings).Select(x => x.ToString()); + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_outOfRangeMultipleItemsMaximum", validationResult.ErrorMessage); + } + } + + [Test] + public void Max_Item_Validation_Respects_0_As_Unlimited() + { + var value = Enumerable.Range(1, 100).Select(x => x.ToString()); + var editor = CreateValueEditor(); + editor.ConfigurationObject = new MultipleTextStringConfiguration(); + + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + Assert.IsEmpty(result); + } + private static object? FromEditor(object? value, int max = 0) => CreateValueEditor().FromEditor(new ContentPropertyData(value, new MultipleTextStringConfiguration { Max = max }), null); @@ -129,11 +197,25 @@ public class MultipleTextStringValueEditorTests private static MultipleTextStringPropertyEditor.MultipleTextStringPropertyValueEditor CreateValueEditor() { - var valueEditor = new MultipleTextStringPropertyEditor.MultipleTextStringPropertyValueEditor( + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + return new MultipleTextStringPropertyEditor.MultipleTextStringPropertyValueEditor( Mock.Of(), Mock.Of(), Mock.Of(), - new DataEditorAttribute("alias")); - return valueEditor; + new DataEditorAttribute("alias"), + localizedTextServiceMock.Object) + { + ConfigurationObject = new MultipleTextStringConfiguration + { + Min = 2, + Max = 4 + }, + }; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RadioButtonsPropertyValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RadioButtonsPropertyValueEditorTests.cs new file mode 100644 index 0000000000..4b36a8cc73 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RadioButtonsPropertyValueEditorTests.cs @@ -0,0 +1,60 @@ +using System.Globalization; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class RadioButtonsPropertyValueEditorTests +{ + [TestCase("Red", true)] + [TestCase("Yellow", false)] + public void Validates_Is_One_Of_Options(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_notOneOfOptions", validationResult.ErrorMessage); + } + } + + private static RadioButtonsPropertyEditor.RadioButtonsPropertyValueEditor CreateValueEditor() + { + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + + var configuration = new ValueListConfiguration + { + Items = ["Red", "Green", "Blue"], + }; + + return new RadioButtonsPropertyEditor.RadioButtonsPropertyValueEditor( + Mock.Of(), + Mock.Of(), + Mock.Of(), + new DataEditorAttribute("alias"), + localizedTextServiceMock.Object) + { + ConfigurationObject = configuration + }; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderPropertyValueEditorTests.cs similarity index 88% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderPropertyValueEditorTests.cs index 7adda7a525..744f2b59c0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderPropertyValueEditorTests.cs @@ -8,7 +8,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Serialization; @@ -16,7 +15,7 @@ using Umbraco.Cms.Infrastructure.Serialization; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class SliderValueEditorTests +public class SliderPropertyValueEditorTests { #pragma warning disable IDE1006 // Naming Styles public static object[] InvalidCaseData = new object[] @@ -127,7 +126,7 @@ public class SliderValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, "validation_unexpectedRange"); + Assert.AreEqual("validation_unexpectedRange", validationResult.ErrorMessage); } } @@ -152,7 +151,7 @@ public class SliderValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, "validation_invalidRange"); + Assert.AreEqual("validation_invalidRange", validationResult.ErrorMessage); } } @@ -177,7 +176,7 @@ public class SliderValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMinimum"); + Assert.AreEqual("validation_outOfRangeMinimum", validationResult.ErrorMessage); } } @@ -202,21 +201,22 @@ public class SliderValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMaximum"); + Assert.AreEqual("validation_outOfRangeMaximum", validationResult.ErrorMessage); } } - [TestCase(1.3, 1.7, true)] - [TestCase(1.4, 1.7, false)] - [TestCase(1.3, 1.6, false)] - public void Validates_Matches_Configured_Step(decimal from, decimal to, bool expectedSuccess) + [TestCase(0.2, 1.3, 1.7, true)] + [TestCase(0.2, 1.4, 1.7, false)] + [TestCase(0.2, 1.3, 1.6, false)] + [TestCase(0.0, 1.4, 1.7, true)] // A step of zero would trigger a divide by zero error in evaluating. So we always pass validation for zero, as effectively any step value is valid. + public void Validates_Matches_Configured_Step(decimal step, decimal from, decimal to, bool expectedSuccess) { var value = new JsonObject { { "from", from }, { "to", to }, }; - var editor = CreateValueEditor(); + var editor = CreateValueEditor(step: step); var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); if (expectedSuccess) { @@ -227,7 +227,7 @@ public class SliderValueEditorTests Assert.AreEqual(1, result.Count()); var validationResult = result.First(); - Assert.AreEqual(validationResult.ErrorMessage, "validation_invalidStep"); + Assert.AreEqual("validation_invalidStep", validationResult.ErrorMessage); } } @@ -244,7 +244,7 @@ public class SliderValueEditorTests return CreateValueEditor().ToEditor(property.Object); } - private static SliderPropertyEditor.SliderPropertyValueEditor CreateValueEditor(bool enableRange = true) + private static SliderPropertyEditor.SliderPropertyValueEditor CreateValueEditor(bool enableRange = true, decimal step = 0.2m) { var localizedTextServiceMock = new Mock(); localizedTextServiceMock.Setup(x => x.Localize( @@ -265,7 +265,7 @@ public class SliderValueEditorTests EnableRange = enableRange, MinimumValue = 1.1m, MaximumValue = 1.9m, - Step = 0.2m + Step = step }, }; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/BlockListValueRequiredValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/BlockListValueRequiredValidatorTests.cs new file mode 100644 index 0000000000..3256f9d15d --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/BlockListValueRequiredValidatorTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Text.Json.Nodes; +using NUnit.Framework; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Infrastructure.PropertyEditors.Validators; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class BlockListValueRequiredValidatorTests +{ + [Test] + public void Validates_Empty_Block_List_As_Not_Provided() + { + var validator = new BlockListValueRequiredValidator(new SystemTextJsonSerializer()); + + var value = JsonNode.Parse("{ \"contentData\": [], \"settingsData\": [] }"); + var result = validator.ValidateRequired(value, ValueTypes.Json); + Assert.AreEqual(1, result.Count()); + } + + [Test] + public void Validates_Populated_Block_List_As_Provided() + { + var validator = new BlockListValueRequiredValidator(new SystemTextJsonSerializer()); + + var value = JsonNode.Parse("{ \"contentData\": [ {} ], \"settingsData\": [] }"); + var result = validator.ValidateRequired(value, ValueTypes.Json); + Assert.IsEmpty(result); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/EmailValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/EmailValidatorTests.cs new file mode 100644 index 0000000000..24d8bc67a7 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/EmailValidatorTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Globalization; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors.Validators; + +[TestFixture] +public class EmailValidatorTests +{ + [TestCase(null, true)] + [TestCase("", true)] + [TestCase(" ", false)] + [TestCase("test@test.com", true)] + [TestCase("invalid", false)] + public void Validates_Email_Address(object? email, bool expectedSuccess) + { + var validator = CreateValidator(); + var result = validator.Validate(email, ValueTypes.String, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_invalidEmail", validationResult.ErrorMessage); + } + } + + private static EmailValidator CreateValidator() + { + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + return new EmailValidator(localizedTextServiceMock.Object); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/MultiUrlPickerValueEditorValidationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/MultiUrlPickerValueEditorValidationTests.cs new file mode 100644 index 0000000000..c2d4120ef3 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/MultiUrlPickerValueEditorValidationTests.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors.Validators; + +[TestFixture] +internal class MultiUrlPickerValueEditorValidationTests +{ + [TestCase(1, true, "[{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"}]")] + [TestCase(2, false, "[{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"}]")] + [TestCase(1, true, "[{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"},{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"}]")] + [TestCase(3, false, "[{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"},{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"}]")] + [TestCase(1, false, "[]")] + [TestCase(1, false, null)] + public void Validates_Min_Limit(int min, bool succeed, string? value) + { + var picker = CreateValueEditor(); + + picker.ConfigurationObject = new MultiUrlPickerConfiguration() { MinNumber = min }; + + var result = picker.Validate(value, false, null, PropertyValidationContext.Empty()); + ValidateResult(succeed, result); + } + + [TestCase(1, true, "[{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"}]")] + [TestCase(1, false, "[{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"},{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"}]")] + [TestCase(3, true, "[{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"},{\"icon\":\"icon-document\",\"name\":\"Page 1\",\"published\":true,\"queryString\":null,\"target\":null,\"trashed\":false,\"type\":\"document\",\"unique\":\"7d285be2-7cd5-4c7b-a252-b064e31f049f\",\"url\":\"/\"}]")] + [TestCase(1, true, "[]")] + [TestCase(1, true, null)] + public void Validates_Max_Limit(int max, bool succeed, string? value) + { + var picker = CreateValueEditor(); + + picker.ConfigurationObject = new MultiUrlPickerConfiguration() { MaxNumber = max }; + + var result = picker.Validate(value, false, null, PropertyValidationContext.Empty()); + ValidateResult(succeed, result); + } + + private static void ValidateResult(bool succeed, IEnumerable result) + { + if (succeed) + { + Assert.IsEmpty(result); + } + else + { + Assert.That(result.Count(), Is.EqualTo(1)); + } + } + + private static MultiUrlPickerValueEditor CreateValueEditor() => + new( + Mock.Of>(), + Mock.Of(), + Mock.Of(), + new DataEditorAttribute("alias"), + Mock.Of(), + new SystemTextJsonSerializer(), + Mock.Of(), + Mock.Of(), + Mock.Of()) + { + ConfigurationObject = new MultiUrlPickerConfiguration(), + }; +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/RequiredValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/RequiredValidatorTests.cs new file mode 100644 index 0000000000..d2071472ab --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/RequiredValidatorTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.ComponentModel.DataAnnotations; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.Validators; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors.Validators; + +[TestFixture] +public class RequiredValidatorTests +{ + [Test] + public void Validates_Null() + { + var validator = new RequiredValidator(); + var result = validator.ValidateRequired(null, ValueTypes.String); + AssertValidationFailed(result, expectedMessage: Constants.Validation.ErrorMessages.Properties.Missing); + } + + [TestCase("", false)] + [TestCase(" ", false)] + [TestCase("a", true)] + public void Validates_Strings(string value, bool expectedSuccess) + { + var validator = new RequiredValidator(); + var result = validator.ValidateRequired(value, ValueTypes.String); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + AssertValidationFailed(result); + } + } + + [TestCase("{}", false)] + [TestCase("[]", false)] + [TestCase("{ }", false)] + [TestCase("[ ]", false)] + [TestCase(" { } ", false)] + [TestCase(" [ ] ", false)] + [TestCase(" { \"foo\": \"bar\" } ", true)] + public void Validates_Json(string value, bool expectedSuccess) + { + var validator = new RequiredValidator(); + var result = validator.ValidateRequired(value, ValueTypes.Json); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + AssertValidationFailed(result); + } + } + + private static void AssertValidationFailed(IEnumerable result, string expectedMessage = Constants.Validation.ErrorMessages.Properties.Empty) + { + Assert.AreEqual(1, result.Count()); + Assert.AreEqual(expectedMessage, result.First().ErrorMessage); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TextOnlyValueEditorValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TextOnlyValueEditorValidatorTests.cs new file mode 100644 index 0000000000..14fa669b72 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TextOnlyValueEditorValidatorTests.cs @@ -0,0 +1,66 @@ +using System.ComponentModel.DataAnnotations; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors.Validators; + +[TestFixture] +internal class TextOnlyValueEditorValidatorTests +{ + internal enum ConfigurationType + { + TextAreaConfiguration, + TextboxConfiguration, + } + + [TestCase(true, ConfigurationType.TextboxConfiguration, null, "123")] + [TestCase(true, ConfigurationType.TextAreaConfiguration, null, "123")] + [TestCase(false, ConfigurationType.TextAreaConfiguration, 2, "123")] + [TestCase(false, ConfigurationType.TextboxConfiguration, 2, "123")] + [TestCase(true, ConfigurationType.TextboxConfiguration, 10, "123")] + [TestCase(true, ConfigurationType.TextAreaConfiguration, 10, "123")] + public void Validates_String_Length(bool shouldSucceed, ConfigurationType configurationType, int? maxChars, string value) + { + var editor = CreateValueEditor(); + + editor.ConfigurationObject = CreateConfiguration(configurationType, maxChars); + + var results = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + + ValidateResult(shouldSucceed, results); + } + + private static object CreateConfiguration(ConfigurationType type, int? maxChars) => + type switch + { + ConfigurationType.TextboxConfiguration => new TextboxConfiguration { MaxChars = maxChars }, + ConfigurationType.TextAreaConfiguration => new TextAreaConfiguration { MaxChars = maxChars }, + _ => throw new InvalidOperationException(), + }; + + private static void ValidateResult(bool succeed, IEnumerable result) + { + if (succeed) + { + Assert.IsEmpty(result); + } + else + { + Assert.IsNotEmpty(result); + } + } + + private TextOnlyValueEditor CreateValueEditor() => + new( + new DataEditorAttribute("alias"), + Mock.Of(), + Mock.Of(), + new SystemTextJsonSerializer(), + Mock.Of()); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TrueFalseValueRequiredValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TrueFalseValueRequiredValidatorTests.cs new file mode 100644 index 0000000000..a7dfa7fe7b --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TrueFalseValueRequiredValidatorTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using NUnit.Framework; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Infrastructure.PropertyEditors.Validators; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class TrueFalseValueRequiredValidatorTests +{ + [Test] + public void Validates_Null_Value_As_Not_Provided() + { + var validator = new TrueFalseValueRequiredValidator(); + + var result = validator.ValidateRequired(null, ValueTypes.Integer); + Assert.AreEqual(1, result.Count()); + } + + [Test] + public void Validates_False_Value_As_Not_Provided() + { + var validator = new TrueFalseValueRequiredValidator(); + + var result = validator.ValidateRequired(false, ValueTypes.Integer); + Assert.AreEqual(1, result.Count()); + } + + [Test] + public void Validates_True_Value_As_Provided() + { + var validator = new TrueFalseValueRequiredValidator(); + + var result = validator.ValidateRequired(true, ValueTypes.Integer); + Assert.IsEmpty(result); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs index 70c6c95ebe..097a0495e5 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs @@ -279,7 +279,7 @@ public class PropertyValidationServiceTests IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper) - : base(attribute, shortStringHelper, jsonSerializer, ioHelper) + : base(attribute, Mock.Of(), shortStringHelper, jsonSerializer, ioHelper) { } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs index 6f3d2a7356..baa49a7632 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using System.Text; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; @@ -118,6 +116,102 @@ namespace Umbraco.Cms.Web.Common.PublishedModels Assert.AreEqual(expected.ClearLf(), gen); } + [Test] + public void GenerateSimpleType_WithoutVersion() + { + // Umbraco returns nice, pascal-cased names. + var type1 = new TypeModel + { + Id = 1, + Alias = "type1", + ClrName = "Type1", + Name = "type1Name", + ParentId = 0, + BaseType = null, + ItemType = TypeModel.ItemTypes.Content, + }; + type1.Properties.Add(new PropertyModel + { + Alias = "prop1", + ClrName = "Prop1", + Name = "prop1Name", + ModelClrType = typeof(string), + }); + + TypeModel[] types = { type1 }; + + var modelsBuilderConfig = new ModelsBuilderSettings { IncludeVersionNumberInGeneratedModels = false }; + var builder = new TextBuilder(modelsBuilderConfig, types); + + var sb = new StringBuilder(); + builder.Generate(sb, builder.GetModelsToGenerate().First()); + var gen = sb.ToString(); + + var expected = @"//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Umbraco.ModelsBuilder.Embedded +// +// Changes to this file will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Linq.Expressions; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.ModelsBuilder; +using Umbraco.Cms.Core; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.PublishedModels +{ + /// type1Name + [PublishedModel(""type1"")] + public partial class Type1 : PublishedContentModel + { + // helpers +#pragma warning disable 0109 // new is redundant + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """")] + public new const string ModelTypeAlias = ""type1""; + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """")] + public new const PublishedItemType ModelItemType = PublishedItemType.Content; + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """")] + [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] + public new static IPublishedContentType GetModelContentType(IPublishedContentTypeCache contentTypeCache) + => PublishedModelUtility.GetModelContentType(contentTypeCache, ModelItemType, ModelTypeAlias); + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """")] + [return: global::System.Diagnostics.CodeAnalysis.MaybeNull] + public static IPublishedPropertyType GetModelPropertyType(IPublishedContentTypeCache contentTypeCache, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(contentTypeCache), selector); +#pragma warning restore 0109 + + private IPublishedValueFallback _publishedValueFallback; + + // ctor + public Type1(IPublishedContent content, IPublishedValueFallback publishedValueFallback) + : base(content, publishedValueFallback) + { + _publishedValueFallback = publishedValueFallback; + } + + // properties + + /// + /// prop1Name + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """")] + [global::System.Diagnostics.CodeAnalysis.MaybeNull] + [ImplementPropertyType(""prop1"")] + public virtual string Prop1 => this.Value(_publishedValueFallback, ""prop1""); + } +} +"; + Console.WriteLine(gen); + Assert.AreEqual(expected.ClearLf(), gen); + } + [Test] public void GenerateSimpleType_Ambiguous_Issue() {