diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs index 74ce67ed03..219ce30e78 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Security/BackOfficeController.cs @@ -2,14 +2,19 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Extensions; using Umbraco.New.Cms.Web.Common.Routing; +using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult; +using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult; namespace Umbraco.Cms.ManagementApi.Controllers.Security; @@ -21,12 +26,18 @@ public class BackOfficeController : ManagementApiControllerBase private readonly IHttpContextAccessor _httpContextAccessor; private readonly IBackOfficeSignInManager _backOfficeSignInManager; private readonly IBackOfficeUserManager _backOfficeUserManager; + private readonly IOptions _securitySettings; - public BackOfficeController(IHttpContextAccessor httpContextAccessor, IBackOfficeSignInManager backOfficeSignInManager, IBackOfficeUserManager backOfficeUserManager) + public BackOfficeController( + IHttpContextAccessor httpContextAccessor, + IBackOfficeSignInManager backOfficeSignInManager, + IBackOfficeUserManager backOfficeUserManager, + IOptions securitySettings) { _httpContextAccessor = httpContextAccessor; _backOfficeSignInManager = backOfficeSignInManager; _backOfficeUserManager = backOfficeUserManager; + _securitySettings = securitySettings; } [HttpGet("authorize")] @@ -41,36 +52,93 @@ public class BackOfficeController : ManagementApiControllerBase return BadRequest("Unable to obtain OpenID data from the current request"); } + return request.IdentityProvider.IsNullOrWhiteSpace() + ? await AuthorizeInternal(request) + : await AuthorizeExternal(request); + } + + private async Task AuthorizeInternal(OpenIddictRequest request) + { + // TODO: ensure we handle sign-in notifications for internal logins. + // when the new login screen is implemented for internal logins, make sure it still handles + // user sign-in notifications (calls BackOfficeSignInManager.HandleSignIn) as part of the + // sign-in process + // for future reference, notifications are already handled for the external login flow by + // by calling BackOfficeSignInManager.ExternalLoginSignInAsync + // retrieve the user principal stored in the authentication cookie. AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); - if (cookieAuthResult.Succeeded && cookieAuthResult.Principal?.Identity?.Name != null) + var userName = cookieAuthResult.Succeeded + ? cookieAuthResult.Principal?.Identity?.Name + : null; + + if (userName != null) { - BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(cookieAuthResult.Principal.Identity.Name); + BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByNameAsync(userName); if (backOfficeUser != null) { - ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); - backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Key.ToString()); - - // TODO: it is not optimal to append all claims to the token. - // the token size grows with each claim, although it is still smaller than the old cookie. - // see if we can find a better way so we do not risk leaking sensitive data in bearer tokens. - // maybe work with scopes instead? - Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); - foreach (Claim backOfficeClaim in backOfficeClaims) - { - backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); - } - - if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess)) - { - // "offline_access" scope is required to use refresh tokens - backOfficePrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess); - } - - return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); + return await SignInBackOfficeUser(backOfficeUser, request); } } - return new ChallengeResult(new[] { Constants.Security.BackOfficeAuthenticationType }); + return DefaultChallengeResult(); } + + private async Task AuthorizeExternal(OpenIddictRequest request) + { + var provider = request.IdentityProvider ?? throw new ArgumentException("No identity provider found in request", nameof(request)); + + ExternalLoginInfo? loginInfo = await _backOfficeSignInManager.GetExternalLoginInfoAsync(); + if (loginInfo?.Principal != null) + { + IdentitySignInResult result = await _backOfficeSignInManager.ExternalLoginSignInAsync(loginInfo, false, _securitySettings.Value.UserBypassTwoFactorForExternalLogins); + + if (result.Succeeded) + { + // Update any authentication tokens if succeeded + await _backOfficeSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo); + + // sign in the backoffice user associated with the login provider and unique provider key + BackOfficeIdentityUser? backOfficeUser = await _backOfficeUserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); + if (backOfficeUser != null) + { + return await SignInBackOfficeUser(backOfficeUser, request); + } + } + else + { + // avoid infinite auth loops when something fails by performing the default challenge (default login screen) + return DefaultChallengeResult(); + } + } + + AuthenticationProperties properties = _backOfficeSignInManager.ConfigureExternalAuthenticationProperties(provider, null); + return new ChallengeResult(provider, properties); + } + + private async Task SignInBackOfficeUser(BackOfficeIdentityUser backOfficeUser, OpenIddictRequest request) + { + ClaimsPrincipal backOfficePrincipal = await _backOfficeSignInManager.CreateUserPrincipalAsync(backOfficeUser); + backOfficePrincipal.SetClaim(OpenIddictConstants.Claims.Subject, backOfficeUser.Key.ToString()); + + // TODO: it is not optimal to append all claims to the token. + // the token size grows with each claim, although it is still smaller than the old cookie. + // see if we can find a better way so we do not risk leaking sensitive data in bearer tokens. + // maybe work with scopes instead? + Claim[] backOfficeClaims = backOfficePrincipal.Claims.ToArray(); + foreach (Claim backOfficeClaim in backOfficeClaims) + { + backOfficeClaim.SetDestinations(OpenIddictConstants.Destinations.AccessToken); + } + + if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess)) + { + // "offline_access" scope is required to use refresh tokens + backOfficePrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess); + } + + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, backOfficePrincipal); + } + + private static IActionResult DefaultChallengeResult() => new ChallengeResult(Constants.Security.BackOfficeAuthenticationType); } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj index 7d477069ed..24d17d34a3 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj +++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj index 056eb09513..922be06acc 100644 --- a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj +++ b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj @@ -50,4 +50,12 @@ + + + + + + + + diff --git a/src/Umbraco.Cms.Targets/buildTransitive/Umbraco.Cms.Targets.props b/src/Umbraco.Cms.Targets/buildTransitive/Umbraco.Cms.Targets.props index 6168686bcb..b79d9f9a4b 100644 --- a/src/Umbraco.Cms.Targets/buildTransitive/Umbraco.Cms.Targets.props +++ b/src/Umbraco.Cms.Targets/buildTransitive/Umbraco.Cms.Targets.props @@ -5,7 +5,7 @@ $(DefaultItemExcludes);umbraco\Logs\** $(DefaultItemExcludes);wwwroot\media\** - + diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj index e1a0e2665c..1ff6e848ad 100644 --- a/src/Umbraco.Cms/Umbraco.Cms.csproj +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -1,4 +1,4 @@ - + Umbraco CMS Installs Umbraco CMS with all default dependencies in your ASP.NET Core project. @@ -12,4 +12,12 @@ + + + + + + + + diff --git a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs index 37b671926c..90498991a4 100644 --- a/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/LoggingSettings.cs @@ -2,6 +2,8 @@ // See LICENSE for more details. using System.ComponentModel; +using Microsoft.Extensions.Hosting; +using Umbraco.Cms.Core.Extensions; namespace Umbraco.Cms.Core.Configuration.Models; @@ -12,10 +14,28 @@ namespace Umbraco.Cms.Core.Configuration.Models; public class LoggingSettings { internal const string StaticMaxLogAge = "1.00:00:00"; // TimeSpan.FromHours(24); + internal const string StaticDirectory = Constants.SystemDirectories.LogFiles; /// /// Gets or sets a value for the maximum age of a log file. /// [DefaultValue(StaticMaxLogAge)] public TimeSpan MaxLogAge { get; set; } = TimeSpan.Parse(StaticMaxLogAge); + + /// + /// Gets or sets the folder to use for log files + /// + [DefaultValue(StaticDirectory)] + public string Directory { get; set; } = StaticDirectory; + + public string GetAbsoluteLoggingPath(IHostEnvironment hostEnvironment) + { + var dir = Directory; + if (dir.StartsWith("~/")) + { + return hostEnvironment.MapPathContentRoot(dir); + } + + return dir; + } } diff --git a/src/Umbraco.Core/Constants-Marketplace.cs b/src/Umbraco.Core/Constants-Marketplace.cs index 6d5abb38e7..38cf282b74 100644 --- a/src/Umbraco.Core/Constants-Marketplace.cs +++ b/src/Umbraco.Core/Constants-Marketplace.cs @@ -7,6 +7,6 @@ public static partial class Constants /// public static class Marketplace { - public const string Url = "https://dev.marketplace.umbraco.com"; + public const string Url = "https://marketplace.umbraco.com"; } } diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index 85375390ac..d0e4488e0e 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -60,6 +60,7 @@ public static partial class Constants /// /// The default folder where Umbraco log files are stored /// + [Obsolete("Use LoggingSettings.GetLoggingDirectory() instead, will be removed in Umbraco 13.")] public const string LogFiles = Umbraco + "/Logs"; [Obsolete("Use PluginIcons instead")] diff --git a/src/Umbraco.Core/Deploy/DataTypeConfigurationConnectorExtensions.cs b/src/Umbraco.Core/Deploy/DataTypeConfigurationConnectorExtensions.cs new file mode 100644 index 0000000000..dbd501d277 --- /dev/null +++ b/src/Umbraco.Core/Deploy/DataTypeConfigurationConnectorExtensions.cs @@ -0,0 +1,48 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Extension methods adding backwards-compatability between and . +/// +/// +/// These extension methods will be removed in Umbraco 13. +/// +public static class DataTypeConfigurationConnectorExtensions +{ + /// + /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. + /// + /// The connector. + /// The data type. + /// The dependencies. + /// The context cache. + /// + /// The artifact configuration value. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static string? ToArtifact(this IDataTypeConfigurationConnector connector, IDataType dataType, ICollection dependencies, IContextCache contextCache) + => connector is IDataTypeConfigurationConnector2 connector2 + ? connector2.ToArtifact(dataType, dependencies, contextCache) + : connector.ToArtifact(dataType, dependencies); + + /// + /// Gets the data type configuration corresponding to an artifact configuration value. + /// + /// The connector. + /// The data type. + /// The artifact configuration value. + /// The context cache. + /// + /// The data type configuration. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static object? FromArtifact(this IDataTypeConfigurationConnector connector, IDataType dataType, string? configuration, IContextCache contextCache) + => connector is IDataTypeConfigurationConnector2 connector2 + ? connector2.FromArtifact(dataType, configuration, contextCache) + : connector.FromArtifact(dataType, configuration); +} diff --git a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs index 506d1f5745..36302efd07 100644 --- a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs +++ b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs @@ -27,20 +27,8 @@ public interface IDataTypeConfigurationConnector /// /// The artifact configuration value. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - string? ToArtifact(IDataType dataType, ICollection dependencies) - => ToArtifact(dataType, dependencies, PassThroughCache.Instance); - - /// - /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. - /// - /// The data type. - /// The dependencies. - /// The context cache. - /// - /// The artifact configuration value. - /// - string? ToArtifact(IDataType dataType, ICollection dependencies, IContextCache contextCache); + [Obsolete($"Implement {nameof(IDataTypeConfigurationConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? ToArtifact(IDataType dataType, ICollection dependencies); /// /// Gets the data type configuration corresponding to an artifact configuration value. @@ -50,18 +38,6 @@ public interface IDataTypeConfigurationConnector /// /// The data type configuration. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - object? FromArtifact(IDataType dataType, string? configuration) - => FromArtifact(dataType, configuration, PassThroughCache.Instance); - - /// - /// Gets the data type configuration corresponding to an artifact configuration value. - /// - /// The data type. - /// The artifact configuration value. - /// The context cache. - /// - /// The data type configuration. - /// - object? FromArtifact(IDataType dataType, string? configuration, IContextCache contextCache); + [Obsolete($"Implement {nameof(IDataTypeConfigurationConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + object? FromArtifact(IDataType dataType, string? configuration); } diff --git a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector2.cs b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector2.cs new file mode 100644 index 0000000000..772bc35dc4 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector2.cs @@ -0,0 +1,56 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// +/// This interface will be merged back into and removed in Umbraco 13. +/// +public interface IDataTypeConfigurationConnector2 : IDataTypeConfigurationConnector +{ + /// + /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. + /// + /// The data type. + /// The dependencies. + /// + /// The artifact configuration value. + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? IDataTypeConfigurationConnector.ToArtifact(IDataType dataType, ICollection dependencies) + => ToArtifact(dataType, dependencies, PassThroughCache.Instance); + + /// + /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. + /// + /// The data type. + /// The dependencies. + /// The context cache. + /// + /// The artifact configuration value. + /// + string? ToArtifact(IDataType dataType, ICollection dependencies, IContextCache contextCache); + + /// + /// Gets the data type configuration corresponding to an artifact configuration value. + /// + /// The data type. + /// The artifact configuration value. + /// + /// The data type configuration. + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + object? IDataTypeConfigurationConnector.FromArtifact(IDataType dataType, string? configuration) + => FromArtifact(dataType, configuration, PassThroughCache.Instance); + + /// + /// Gets the data type configuration corresponding to an artifact configuration value. + /// + /// The data type. + /// The artifact configuration value. + /// The context cache. + /// + /// The data type configuration. + /// + object? FromArtifact(IDataType dataType, string? configuration, IContextCache contextCache); +} diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs index b4f530fb35..adf4c57502 100644 --- a/src/Umbraco.Core/Deploy/IServiceConnector.cs +++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs @@ -15,19 +15,8 @@ public interface IServiceConnector : IDiscoverable /// /// The corresponding artifact, or null. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - IArtifact? GetArtifact(Udi udi) - => GetArtifact(udi, PassThroughCache.Instance); - - /// - /// Gets an artifact. - /// - /// The entity identifier of the artifact. - /// The context cache. - /// - /// The corresponding artifact, or null. - /// - IArtifact? GetArtifact(Udi udi, IContextCache contextCache); + [Obsolete($"Implement {nameof(IServiceConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + IArtifact? GetArtifact(Udi udi); /// /// Gets an artifact. @@ -36,19 +25,8 @@ public interface IServiceConnector : IDiscoverable /// /// The corresponding artifact. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - IArtifact GetArtifact(object entity) - => GetArtifact(entity, PassThroughCache.Instance); - - /// - /// Gets an artifact. - /// - /// The entity. - /// The context cache. - /// - /// The corresponding artifact. - /// - IArtifact GetArtifact(object entity, IContextCache contextCache); + [Obsolete($"Implement {nameof(IServiceConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + IArtifact GetArtifact(object entity); /// /// Initializes processing for an artifact. diff --git a/src/Umbraco.Core/Deploy/IServiceConnector2.cs b/src/Umbraco.Core/Deploy/IServiceConnector2.cs new file mode 100644 index 0000000000..6c1558a956 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IServiceConnector2.cs @@ -0,0 +1,38 @@ +namespace Umbraco.Cms.Core.Deploy; + +/// +/// +/// This interface will be merged back into and removed in Umbraco 13. +/// +public interface IServiceConnector2 : IServiceConnector +{ + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + IArtifact? IServiceConnector.GetArtifact(Udi udi) + => GetArtifact(udi, PassThroughCache.Instance); + + /// + /// Gets an artifact. + /// + /// The entity identifier of the artifact. + /// The context cache. + /// + /// The corresponding artifact, or null. + /// + IArtifact? GetArtifact(Udi udi, IContextCache contextCache); + + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + IArtifact IServiceConnector.GetArtifact(object entity) + => GetArtifact(entity, PassThroughCache.Instance); + + /// + /// Gets an artifact. + /// + /// The entity. + /// The context cache. + /// + /// The corresponding artifact. + /// + IArtifact GetArtifact(object entity, IContextCache contextCache); +} diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs index 9f3b17f71c..fe28e41017 100644 --- a/src/Umbraco.Core/Deploy/IValueConnector.cs +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -29,21 +29,8 @@ public interface IValueConnector /// /// The deploy property value. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies) - => ToArtifact(value, propertyType, dependencies, PassThroughCache.Instance); - - /// - /// Gets the deploy property value corresponding to a content property value, and gather dependencies. - /// - /// The content property value. - /// The value property type - /// The content dependencies. - /// The context cache. - /// - /// The deploy property value. - /// - string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies, IContextCache contextCache); + [Obsolete($"Implement {nameof(IValueConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies); /// /// Gets the content property value corresponding to a deploy property value. @@ -54,19 +41,6 @@ public interface IValueConnector /// /// The content property value. /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue) - => FromArtifact(value, propertyType, currentValue, PassThroughCache.Instance); - - /// - /// Gets the content property value corresponding to a deploy property value. - /// - /// The deploy property value. - /// The value property type - /// The current content property value. - /// The context cache. - /// - /// The content property value. - /// - object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue, IContextCache contextCache); + [Obsolete($"Implement {nameof(IValueConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue); } diff --git a/src/Umbraco.Core/Deploy/IValueConnector2.cs b/src/Umbraco.Core/Deploy/IValueConnector2.cs new file mode 100644 index 0000000000..a0c99dca06 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IValueConnector2.cs @@ -0,0 +1,44 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// +/// This interface will be merged back into and removed in Umbraco 13. +/// +public interface IValueConnector2 : IValueConnector +{ + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? IValueConnector.ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies) + => ToArtifact(value, propertyType, dependencies, PassThroughCache.Instance); + + /// + /// Gets the deploy property value corresponding to a content property value, and gather dependencies. + /// + /// The content property value. + /// The value property type + /// The content dependencies. + /// The context cache. + /// + /// The deploy property value. + /// + string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies, IContextCache contextCache); + + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + object? IValueConnector.FromArtifact(string? value, IPropertyType propertyType, object? currentValue) + => FromArtifact(value, propertyType, currentValue, PassThroughCache.Instance); + + /// + /// Gets the content property value corresponding to a deploy property value. + /// + /// The deploy property value. + /// The value property type + /// The current content property value. + /// The context cache. + /// + /// The content property value. + /// + object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue, IContextCache contextCache); +} diff --git a/src/Umbraco.Core/Deploy/ServiceConnectorExtensions.cs b/src/Umbraco.Core/Deploy/ServiceConnectorExtensions.cs new file mode 100644 index 0000000000..0d0000f97c --- /dev/null +++ b/src/Umbraco.Core/Deploy/ServiceConnectorExtensions.cs @@ -0,0 +1,44 @@ +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Extension methods adding backwards-compatability between and . +/// +/// +/// These extension methods will be removed in Umbraco 13. +/// +public static class ServiceConnectorExtensions +{ + /// + /// Gets an artifact. + /// + /// The connector. + /// The entity identifier of the artifact. + /// The context cache. + /// + /// The corresponding artifact, or null. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static IArtifact? GetArtifact(this IServiceConnector connector, Udi udi, IContextCache contextCache) + => connector is IServiceConnector2 connector2 + ? connector2.GetArtifact(udi, contextCache) + : connector.GetArtifact(udi); + + /// + /// Gets an artifact. + /// + /// The connector. + /// The entity. + /// The context cache. + /// + /// The corresponding artifact. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static IArtifact GetArtifact(this IServiceConnector connector, object entity, IContextCache contextCache) + => connector is IServiceConnector2 connector2 + ? connector2.GetArtifact(entity, contextCache) + : connector.GetArtifact(entity); +} diff --git a/src/Umbraco.Core/Deploy/ValueConnectorExtensions.cs b/src/Umbraco.Core/Deploy/ValueConnectorExtensions.cs new file mode 100644 index 0000000000..eadcee55e0 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ValueConnectorExtensions.cs @@ -0,0 +1,50 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Extension methods adding backwards-compatability between and . +/// +/// +/// These extension methods will be removed in Umbraco 13. +/// +public static class ValueConnectorExtensions +{ + /// + /// Gets the artifact value corresponding to a property value and gather dependencies. + /// + /// The connector. + /// The property value. + /// The property type. + /// The dependencies. + /// The context cache. + /// + /// The artifact value. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static string? ToArtifact(this IValueConnector connector, object? value, IPropertyType propertyType, ICollection dependencies, IContextCache contextCache) + => connector is IValueConnector2 connector2 + ? connector2.ToArtifact(value, propertyType, dependencies, contextCache) + : connector.ToArtifact(value, propertyType, dependencies); + + /// + /// Gets the property value corresponding to an artifact value. + /// + /// The connector. + /// The artifact value. + /// The property type. + /// The current property value. + /// The context cache. + /// + /// The property value. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static object? FromArtifact(this IValueConnector connector, string? value, IPropertyType propertyType, object? currentValue, IContextCache contextCache) + => connector is IValueConnector2 connector2 + ? connector2.FromArtifact(value, propertyType, currentValue, contextCache) + : connector.FromArtifact(value, propertyType, currentValue); +} diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 6f99a1020f..af7b39849c 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -2784,7 +2784,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Allow in root Make this block available in the root of the layout. Allow in areas - Make this block available within other Blocks. + Make this block available by default within the areas of other Blocks (unless explicit permissions are set for these areas). When empty all Blocks allowed for Areas can be created. Areas Grid Columns for Areas diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 65ef7b9f72..4678acad16 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -2887,7 +2887,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Allow in root Make this block available in the root of the layout. Allow in areas - Make this block available within other Blocks. + Make this block available by default within the areas of other Blocks (unless explicit permissions are set for these areas). When empty all Blocks allowed for Areas can be created. Areas Grid Columns for Areas diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index e19ef083e5..b2cbb5bda0 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -2520,6 +2520,38 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Eigenschap '%0%' gebruikt editor '%1%' die niet ondersteund wordt in blokken. + Geef focus aan het container blok + Identificatie + Validatie + %0% moet minimaal %2% keer aanwezig zijn.]]> + %0%mag maximaal %3% keer aanwezig zijn.]]> + Hoeveelheid blokken + Sta alleen specifiek bloktype toe + Toegestane bloktypes + Definieer de type blokken die zijn toegestaan in dit gebied, en optioneel hoeveel van ieder type aanwezig moet zijn. + Weet je zeker dat je dit gebied wilt verwijderen? + Alle blokken op dit moment aangemaakt binnen dit gebied zullen worden verwijderd. + Lay-out opties + Structuur + Afmetingen + Definiëer een of meer afmetingen, dit maakt het mogelijk blokken te vergroten/verkleinen + Beschikbare kolommen + Definieer de verschillende aantal kolombreedtes dat dit blok mag in nemen. Dit voorkomt niet dat blokken in gebieden met kleine kolombreedtes kan worden geplaatst. + Beschikbare rijen + Definiëer de verschillende aantal rijen dat dit blok mag innemen. + Sta toe in root + Maak dit blok beschikbaar in de root van de lay-out + Sta toe in gebieden + Blok + Blokken + Instellingen + Maak dit blok standaard beschikbaar binnen de gebieden van andere blokken (behalve wanneer expliciete rechten zijn gezet voor deze gebieden). + Gebieden + Gebieden + Geadvanceerd + Rechten + Configureer gebied + Verwijder gebied Wat zijn Inhoudssjablonen? diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 908c0bf2e0..e236980d1d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/Umbraco.Infrastructure/Deploy/GridCellValueConnectorExtensions.cs b/src/Umbraco.Infrastructure/Deploy/GridCellValueConnectorExtensions.cs new file mode 100644 index 0000000000..fec3c07e3c --- /dev/null +++ b/src/Umbraco.Infrastructure/Deploy/GridCellValueConnectorExtensions.cs @@ -0,0 +1,51 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Extension methods adding backwards-compatability between and . +/// +/// +/// These extension methods will be removed in Umbraco 13. +/// +public static class GridCellValueConnectorExtensions +{ + /// + /// Gets the value. + /// + /// The connector. + /// The grid control. + /// The dependencies. + /// The context cache. + /// + /// The value. + /// + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static string? GetValue(this IGridCellValueConnector connector, GridValue.GridControl gridControl, ICollection dependencies, IContextCache contextCache) + => connector is IGridCellValueConnector2 connector2 + ? connector2.GetValue(gridControl, dependencies, contextCache) + : connector.GetValue(gridControl, dependencies); + + /// + /// Sets the value. + /// + /// The connector. + /// The grid control. + /// The context cache. + /// + /// This extension method tries to make use of the on types also implementing . + /// + public static void SetValue(this IGridCellValueConnector connector, GridValue.GridControl gridControl, IContextCache contextCache) + { + if (connector is IGridCellValueConnector2 connector2) + { + connector2.SetValue(gridControl, contextCache); + } + else + { + connector.SetValue(gridControl); + } + } +} diff --git a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs index a9ed5fd84d..4395202a56 100644 --- a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs +++ b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs @@ -1,5 +1,4 @@ using Umbraco.Cms.Core.Models; -using static Umbraco.Cms.Core.Models.GridValue; namespace Umbraco.Cms.Core.Deploy; @@ -37,20 +36,8 @@ public interface IGridCellValueConnector /// /// Note that /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - string? GetValue(GridValue.GridControl gridControl, ICollection dependencies) - => GetValue(gridControl, dependencies, PassThroughCache.Instance); - - /// - /// Gets the value to be deployed from the control value as a string. - /// - /// The control containing the value. - /// The dependencies of the property. - /// The context cache. - /// - /// The grid cell value to be deployed. - /// - string? GetValue(GridValue.GridControl gridControl, ICollection dependencies, IContextCache contextCache); + [Obsolete($"Implement {nameof(IGridCellValueConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? GetValue(GridValue.GridControl gridControl, ICollection dependencies); /// /// Allows you to modify the value of a control being deployed. @@ -60,18 +47,6 @@ public interface IGridCellValueConnector /// Follows the pattern of the property value connectors (). /// The SetValue method is used to modify the value of the . /// - [Obsolete("Use the overload accepting IContextCache instead. This overload will be removed in a future version.")] - void SetValue(GridValue.GridControl gridControl) - => SetValue(gridControl, PassThroughCache.Instance); - - /// - /// Allows you to modify the value of a control being deployed. - /// - /// The control being deployed. - /// The context cache. - /// - /// Follows the pattern of the property value connectors (). - /// The SetValue method is used to modify the value of the . - /// - void SetValue(GridValue.GridControl gridControl, IContextCache contextCache); + [Obsolete($"Implement {nameof(IGridCellValueConnector2)} and use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + void SetValue(GridValue.GridControl gridControl); } diff --git a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector2.cs b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector2.cs new file mode 100644 index 0000000000..44a36677e9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector2.cs @@ -0,0 +1,42 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Deploy; + +/// +/// +/// This interface will be merged back into and removed in Umbraco 13. +/// +public interface IGridCellValueConnector2 : IGridCellValueConnector +{ + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + string? IGridCellValueConnector.GetValue(GridValue.GridControl gridControl, ICollection dependencies) + => GetValue(gridControl, dependencies, PassThroughCache.Instance); + + /// + /// Gets the value to be deployed from the control value as a string. + /// + /// The control containing the value. + /// The dependencies of the property. + /// The context cache. + /// + /// The grid cell value to be deployed. + /// + string? GetValue(GridValue.GridControl gridControl, ICollection dependencies, IContextCache contextCache); + + /// + [Obsolete($"Use the overload accepting {nameof(IContextCache)} instead. This overload will be removed in Umbraco 13.")] + void IGridCellValueConnector.SetValue(GridValue.GridControl gridControl) + => SetValue(gridControl, PassThroughCache.Instance); + + /// + /// Allows you to modify the value of a control being deployed. + /// + /// The control being deployed. + /// The context cache. + /// + /// Follows the pattern of the property value connectors (). + /// The SetValue method is used to modify the value of the . + /// + void SetValue(GridValue.GridControl gridControl, IContextCache contextCache); +} diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index d8c6d1ff8f..59e4eeb105 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -7,6 +7,7 @@ using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Compact; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; @@ -21,7 +22,7 @@ namespace Umbraco.Extensions /// Such as adding ProcessID, Thread, AppDomain etc /// It is highly recommended that you keep/use this default in your own logging config customizations /// - [Obsolete("Please use an alternative method.")] + [Obsolete("Please use an alternative method. This will be removed in Umbraco 13.")] public static LoggerConfiguration MinimalConfiguration( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, @@ -36,7 +37,7 @@ namespace Umbraco.Extensions /// Such as adding ProcessID, Thread, AppDomain etc /// It is highly recommended that you keep/use this default in your own logging config customizations /// - [Obsolete("Please use an alternative method.")] + [Obsolete("Please use an alternative method. This will be removed in Umbraco 13.")] public static LoggerConfiguration MinimalConfiguration( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, @@ -126,6 +127,7 @@ namespace Umbraco.Extensions /// /// The log level you wish the JSON file to collect - default is Verbose (highest) /// The number of days to keep log files. Default is set to null which means all logs are kept + [Obsolete("Will be removed in Umbraco 13.")] public static LoggerConfiguration OutputDefaultTextFile( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, @@ -144,6 +146,31 @@ namespace Umbraco.Extensions return logConfig; } + /// + /// Outputs a .txt format log at /App_Data/Logs/ + /// + /// A Serilog LoggerConfiguration + /// + /// + /// The log level you wish the JSON file to collect - default is Verbose (highest) + public static LoggerConfiguration OutputDefaultTextFile( + this LoggerConfiguration logConfig, + IHostEnvironment hostEnvironment, + LoggingSettings loggingSettings, + LogEventLevel minimumLevel = LogEventLevel.Verbose) + { + //Main .txt logfile - in similar format to older Log4Net output + //Ends with ..txt as Date is inserted before file extension substring + logConfig.WriteTo.File( + Path.Combine(loggingSettings.GetAbsoluteLoggingPath(hostEnvironment), $"UmbracoTraceLog.{Environment.MachineName}..txt"), + shared: true, + rollingInterval: RollingInterval.Day, + restrictedToMinimumLevel: minimumLevel, + retainedFileCountLimit: null, //Setting to null means we keep all files - default is 31 days + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}"); + + return logConfig; + } /// /// Used in config - If renamed or moved to other assembly the config file also has be updated. @@ -195,6 +222,7 @@ namespace Umbraco.Extensions /// The log level you wish the JSON file to collect - default is Verbose (highest) /// /// The number of days to keep log files. Default is set to null which means all logs are kept + [Obsolete("Will be removed in Umbraco 13.")] public static LoggerConfiguration OutputDefaultJsonFile( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, @@ -215,5 +243,33 @@ namespace Umbraco.Extensions return logConfig; } + /// + /// Outputs a CLEF format JSON log at /App_Data/Logs/ + /// + /// A Serilog LoggerConfiguration + /// + /// The logging configuration + /// The log level you wish the JSON file to collect - default is Verbose (highest) + /// The number of days to keep log files. Default is set to null which means all logs are kept + public static LoggerConfiguration OutputDefaultJsonFile( + this LoggerConfiguration logConfig, + IHostEnvironment hostEnvironment, + LoggingSettings loggingSettings, + LogEventLevel minimumLevel = LogEventLevel.Verbose, + int? retainedFileCount = null) + { + // .clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) + // Ends with ..txt as Date is inserted before file extension substring + logConfig.WriteTo.File( + new CompactJsonFormatter(), + Path.Combine(loggingSettings.GetAbsoluteLoggingPath(hostEnvironment) ,$"UmbracoTraceLog.{Environment.MachineName}..json"), + shared: true, + rollingInterval: RollingInterval.Day, // Create a new JSON file every day + retainedFileCountLimit: retainedFileCount, // Setting to null means we keep all files - default is 31 days + restrictedToMinimumLevel: minimumLevel); + + return logConfig; + } + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index d8e132440c..bd3fc6849d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -373,7 +373,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install { Message = "The database configuration failed with the following message: " + ex.Message + - $"\n Please check log file for additional information (can be found in '{Constants.SystemDirectories.LogFiles}')", + $"\n Please check log file for additional information (can be found in '{nameof(LoggingSettings)}.{nameof(LoggingSettings.Directory)}')", Success = false, Percentage = "90" }; diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index f2d8a23b86..a858d53e86 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -13,7 +13,7 @@ - + @@ -24,7 +24,7 @@ - + @@ -32,7 +32,7 @@ - + diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index a4230fca4d..46e69122ac 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -8,9 +8,9 @@ - + - + diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 57082eb0e2..2dc22d54a5 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs index 9a9d606644..a5e30f9011 100644 --- a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -82,8 +82,9 @@ public static class ServiceCollectionExtensions IHostEnvironment hostEnvironment, IConfiguration configuration) { - // TODO: WEBSITE_RUN_FROM_PACKAGE - can't assume this DIR is writable - we have an IConfiguration instance so a later refactor should be easy enough. - var loggingDir = hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles); + LoggingSettings loggerSettings = GetLoggerSettings(configuration); + + var loggingDir = loggerSettings.GetAbsoluteLoggingPath(hostEnvironment); ILoggingConfiguration loggingConfig = new LoggingConfiguration(loggingDir); var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); @@ -146,6 +147,13 @@ public static class ServiceCollectionExtensions return services; } + private static LoggingSettings GetLoggerSettings(IConfiguration configuration) + { + var loggerSettings = new LoggingSettings(); + configuration.GetSection(Constants.Configuration.ConfigLogging).Bind(loggerSettings); + return loggerSettings; + } + /// /// Called to create the to assign to the /// diff --git a/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/img/login.svg b/src/Umbraco.Web.UI.Client/src/assets/img/login.svg similarity index 100% rename from src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/img/login.svg rename to src/Umbraco.Web.UI.Client/src/assets/img/login.svg diff --git a/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.controller.js b/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.controller.js index be8ddba592..2cb075e80d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.controller.js @@ -6,10 +6,9 @@ * @description * The controller for the info view of the datatype editor */ -function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout, editorService) { +function DataTypeInfoController($scope, $routeParams, dataTypeResource, $timeout, editorService) { var vm = this; - var evts = []; var referencesLoaded = false; vm.references = {}; @@ -48,7 +47,7 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe function open(id, event, type) { // targeting a new tab/window? - if (event.ctrlKey || + if (event.ctrlKey || event.shiftKey || event.metaKey || // apple (event.button && event.button === 1) // middle click, >IE9 + everyone else @@ -85,25 +84,7 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe } } - // load data type references when the references tab is activated - evts.push(eventsService.on("app.tabChange", function (event, args) { - $timeout(function () { - if (args.alias === "info") { - loadRelations(); - } - }); - })); - - //ensure to unregister from all events! - $scope.$on('$destroy', function () { - for (var e in evts) { - eventsService.unsubscribe(evts[e]); - } - }); - - - - + loadRelations(); } angular.module("umbraco").controller("Umbraco.Editors.DataType.InfoController", DataTypeInfoController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridblock/gridblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridblock/gridblock.editor.html index 710dd15e53..bceef66e37 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridblock/gridblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridentryeditors/gridblock/gridblock.editor.html @@ -55,19 +55,19 @@ } :host { - --inherited--column-gap: var(--umb-block-grid--column-gap, 10px); - --inherited--row-gap: var(--umb-block-grid--row-gap, 10px); - --inherited--areas-column-gap: var(--umb-block-grid--areas-column-gap, 10px); - --inherited--areas-row-gap: var(--umb-block-grid--areas-row-gap, 10px); + --inherited--areas-column-gap: var(--umb-block-grid--areas-column-gap, 0); + --inherited--areas-row-gap: var(--umb-block-grid--areas-row-gap, 0); } [part='area-container'] { box-sizing: border-box; padding: 10px; - --umb-block-grid--column-gap: var(--inherited--column-gap, 10px); - --umb-block-grid--row-gap: var(--inherited--row-gap, 10px); - --umb-block-grid--areas-column-gap: var(--inherited--areas-column-gap, 10px); - --umb-block-grid--areas-row-gap: var(--inherited--areas-row-gap, 10px); + --umb-block-grid--areas-column-gap: 10px; + --umb-block-grid--areas-row-gap: 10px; + } + [part='area-container'] slot { + --umb-block-grid--areas-column-gap: var(--inherited--areas-column-gap, 0); + --umb-block-grid--areas-row-gap: var(--inherited--areas-row-gap, 0); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less index 693108f05d..4e5275d788 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less @@ -95,18 +95,13 @@ ng-form.ng-invalid-val-server-match-content > .umb-block-grid__block:not(.--acti &::after { display: var(--umb-block-grid--block-ui-display, block); - animation: umb-block-grid__block__border-pulse 400ms ease-in-out alternate infinite; - @keyframes umb-block-grid__block__border-pulse { - 0% { border-color: rgba(@blueDark, 1); } - 100% { border-color: rgba(@blueDark, 0.66); } - } + border-color: @blueDark; } } &.--active { &::after { display: block; border-color: @pinkLight; - animation: none; } > .umb-block-grid__block--context { @@ -223,15 +218,16 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl .umb-block-grid__block--context { position: absolute; - top: -21px; + bottom: 100%; right: -1px; + max-width: 100%; font-size: 12px; z-index: 4; display: var(--umb-block-grid--block-ui-display, flex); justify-content: end; /** prevent interaction with inline-create button just beneath the context-bar: */ - ::after { + &::after { content: ''; position: absolute; top: 100%; @@ -243,34 +239,62 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl .__context-bar { padding: 0 9px; padding-top: 1px; - background-color: #3544B1; + background-color: @blueDark; color: white; border-top: rgba(255, 255, 255, .7) 1px solid; border-left: rgba(255, 255, 255, .7) 1px solid; border-right: rgba(255, 255, 255, .7) 1px solid; border-top-left-radius: 3px; border-top-right-radius: 3px; - display: inline-block; + display: inline-flex; + box-sizing: border-box; + + max-width: 100%; + white-space: nowrap; + + /* Ensure that the bar is visually connected with the border **/ + &::before { + content: ''; + position: absolute; + left: 0; + bottom: -1px; + width: 1px; + border-top: 1px solid @blueDark; + box-sizing: border-box; + background: none; + } + &::after { + content: ''; + position: absolute; + right: 1px; + bottom: -2px; + height: 2px; + width: 2px; + border-top: 1px solid @blueDark; + border-right: 1px solid @blueDark; + box-sizing: border-box; + background: none; + } + .action { color: currentColor; font-size: 12px; + line-height: inherit; + + overflow: hidden; + text-overflow: ellipsis; } .__divider { - display: inline-block; margin: 0 3px; } .__label { - display: inline-flex; align-items: center; padding: 0; font-weight: 700; user-select: none; - - .icon { - font-size: 22px; - margin-right: 5px; - display: inline-block; - } + + overflow: hidden; + text-overflow: ellipsis; } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entry.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entry.html index d8621f7a5b..567d20ff25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entry.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umb-block-grid-entry.html @@ -76,18 +76,10 @@ title="blockEditor_focusParentBlock" ng-click="vm.parentBlock.showBlockUI()" ng-class="{ '--error': false && vm.valFormManager.isShowingValidation() }" - > - - {{vm.parentBlock.label}} - - - Focus parent block - - + ng-bind="vm.parentBlock.label" + >
/
-
- {{vm.layoutEntry.$block.label}} -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationTypes/edit.controller.js index c061bacf26..5dd7f3165b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/edit.controller.js @@ -54,14 +54,7 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, }); // load references when the 'relations' tab is first activated/switched to - var appTabChange = eventsService.on("app.tabChange", function (event, args) { - if (args.alias === "relations") { - loadRelations(); - } - }); - $scope.$on('$destroy', function () { - appTabChange(); - }); + loadRelations(); // Inital page/overview API call of relation type relationTypeResource.getById($routeParams.id) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 0c44f7f6cc..309845da22 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": "^1.0.2", - "@umbraco/playwright-testhelpers": "^1.0.18", + "@umbraco/playwright-testhelpers": "^1.0.19", "camelize": "^1.0.0", "dotenv": "^16.0.2", "faker": "^4.1.0", @@ -138,9 +138,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "1.0.18", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.18.tgz", - "integrity": "sha512-SLtZqs3IxtuBfKBp25ZgAxjrXTIoR/mWQ8qfEFTpsebdQcnZVXxPXbCe2CwPT3zdTpjdjdh/HOfn/ONjtpL8Cw==", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.19.tgz", + "integrity": "sha512-NLZHRrWkfjzLoBc6RagQkbggT1awYARlmHNw50Aih566o+BnzkZ3usW0J8bvU4rW+8BbkClvOqUDCOP5vSYbVA==", "dependencies": { "@umbraco/json-models-builders": "^1.0.2", "camelize": "^1.0.0", @@ -1064,9 +1064,9 @@ } }, "@umbraco/playwright-testhelpers": { - "version": "1.0.18", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.18.tgz", - "integrity": "sha512-SLtZqs3IxtuBfKBp25ZgAxjrXTIoR/mWQ8qfEFTpsebdQcnZVXxPXbCe2CwPT3zdTpjdjdh/HOfn/ONjtpL8Cw==", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.19.tgz", + "integrity": "sha512-NLZHRrWkfjzLoBc6RagQkbggT1awYARlmHNw50Aih566o+BnzkZ3usW0J8bvU4rW+8BbkClvOqUDCOP5vSYbVA==", "requires": { "@umbraco/json-models-builders": "^1.0.2", "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 72a314a060..4b5fd80ae8 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^1.0.2", - "@umbraco/playwright-testhelpers": "^1.0.18", + "@umbraco/playwright-testhelpers": "^1.0.19", "camelize": "^1.0.0", "faker": "^4.1.0", "form-data": "^4.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts index 25aff1c6e8..3f39fdaeee 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts @@ -553,8 +553,9 @@ test.describe('Content tests', () => { // Save and publish await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); - await umbracoUi.isSuccessNotificationVisible(); - + // Added additional time because it could fail on pipeline because it's not saving fast enough + await umbracoUi.isSuccessNotificationVisible({timeout:20000}); + // Assert const expectedContent = '

Acceptance test

' await expect(await umbracoApi.content.verifyRenderedContent('/contentpickercontent', expectedContent, true)).toBeTruthy(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/HelpPanel/systemInformation.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/HelpPanel/systemInformation.spec.ts index fb46e6ee52..9bdc85b37d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/HelpPanel/systemInformation.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/HelpPanel/systemInformation.spec.ts @@ -15,7 +15,7 @@ test.describe('System Information', () => { await umbracoApi.users.setCurrentLanguage(enCulture); }); - async function openSystemInformation(page: Page, umbracoUi : UiHelpers) { + async function openSystemInformation(page: Page, umbracoUi: UiHelpers) { //We have to wait for page to load, if the site is slow await umbracoUi.clickElement(umbracoUi.getGlobalHelp()); await expect(page.locator('.umb-help-list-item').last()).toBeVisible(); @@ -47,4 +47,4 @@ test.describe('System Information', () => { // Close the help panel await page.locator('.umb-button__content').last().click(); }); - }); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Login/login.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Login/login.spec.ts index 25eaa34ae5..d97de439d2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Login/login.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Login/login.spec.ts @@ -1,70 +1,69 @@ import {expect } from '@playwright/test'; import {test} from "@umbraco/playwright-testhelpers"; - test.describe('Login', () => { - test.beforeEach(async ({ page, umbracoApi }, testInfo) => { - await umbracoApi.report.report(testInfo); - await page.goto(process.env.URL + '/umbraco'); - }); - test('Login with correct username and password', async ({page}) => { + test.beforeEach(async ({ page, umbracoApi }, testInfo) => { + await umbracoApi.report.report(testInfo); + await page.goto(process.env.URL + '/umbraco'); + }); + test('Login with correct username and password', async ({page}) => { - let error = page.locator('.text-error'); - await expect(error).toBeHidden(); + let error = page.locator('.text-error'); + await expect(error).toBeHidden(); - // Action - await page.fill('#umb-username', process.env.UMBRACO_USER_LOGIN); - await page.fill('#umb-passwordTwo', process.env.UMBRACO_USER_PASSWORD); - await page.locator('[label-key="general_login"]').click(); - await page.waitForNavigation(); + // Action + await page.fill('#umb-username', process.env.UMBRACO_USER_LOGIN); + await page.fill('#umb-passwordTwo', process.env.UMBRACO_USER_PASSWORD); + await page.locator('[label-key="general_login"]').click(); + await page.waitForNavigation(); - // Assert - await expect(page).toHaveURL(process.env.URL + '/umbraco#/content'); - let usernameField = await page.locator('#umb-username'); - let passwordField = await page.locator('#umb-passwordTwo'); - await expect(usernameField).toHaveCount(0); - await expect(passwordField).toHaveCount(0); - }); + // Assert + await expect(page).toHaveURL(process.env.URL + '/umbraco#/content'); + let usernameField = await page.locator('#umb-username'); + let passwordField = await page.locator('#umb-passwordTwo'); + await expect(usernameField).toHaveCount(0); + await expect(passwordField).toHaveCount(0); + }); - test('Login with correct username but wrong password', async({page}) => { - const username = process.env.UMBRACO_USER_LOGIN; - const password = 'wrong'; + test('Login with correct username but wrong password', async ({page}) => { + const username = process.env.UMBRACO_USER_LOGIN; + const password = 'wrong'; - // Precondition - let error = page.locator('.text-error'); - await expect(error).toBeHidden(); + // Precondition + let error = page.locator('.text-error'); + await expect(error).toBeHidden(); - // Action - await page.fill('#umb-username', username); - await page.fill('#umb-passwordTwo', password); - await page.locator('[label-key="general_login"]').click(); + // Action + await page.fill('#umb-username', username); + await page.fill('#umb-passwordTwo', password); + await page.locator('[label-key="general_login"]').click(); - // Assert - let usernameField = await page.locator('#umb-username'); - let passwordField = await page.locator('#umb-passwordTwo'); - await expect(error).toBeVisible(); - await expect(usernameField).toBeVisible(); - await expect(passwordField).toBeVisible(); - }); + // Assert + let usernameField = await page.locator('#umb-username'); + let passwordField = await page.locator('#umb-passwordTwo'); + await expect(error).toBeVisible(); + await expect(usernameField).toBeVisible(); + await expect(passwordField).toBeVisible(); + }); - test('Login with wrong username and wrong password', async({page}) => { - const username = 'wrong-username'; - const password = 'wrong'; + test('Login with wrong username and wrong password', async ({page}) => { + const username = 'wrong-username'; + const password = 'wrong'; - // Precondition - let error = page.locator('.text-error'); - await expect(error).toBeHidden(); + // Precondition + let error = page.locator('.text-error'); + await expect(error).toBeHidden(); - // Action - await page.fill('#umb-username', username); - await page.fill('#umb-passwordTwo', password); - await page.locator('[label-key="general_login"]').click(); + // Action + await page.fill('#umb-username', username); + await page.fill('#umb-passwordTwo', password); + await page.locator('[label-key="general_login"]').click(); - // Assert - let usernameField = await page.locator('#umb-username'); - let passwordField = await page.locator('#umb-passwordTwo'); - await expect(error).toBeVisible(); - await expect(usernameField).toBeVisible(); - await expect(passwordField).toBeVisible(); - }); + // Assert + let usernameField = await page.locator('#umb-username'); + let passwordField = await page.locator('#umb-passwordTwo'); + await expect(error).toBeVisible(); + await expect(usernameField).toBeVisible(); + await expect(passwordField).toBeVisible(); + }); }); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/mediaSection.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/mediaSection.spec.ts index 2e55aa82b0..ec9dbb5900 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/mediaSection.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/mediaSection.spec.ts @@ -53,7 +53,7 @@ test.describe('Media', () => { // Assert // Needs to wait before refreshing the media tree, otherwise the media files wont be moved to the folder yet - await page.waitForTimeout(1000); + await page.waitForTimeout(2500); await umbracoUi.refreshMediaTree(); await page.locator('[data-element="tree-item-' + folderToMoveTooName + '"]').click(); for (const names of mediaFileTypes) { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts index 0215d617ee..49ffc2afd0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts @@ -32,8 +32,8 @@ test.describe('Modelsbuilder tests', () => { .done() .done() .build(); - await umbracoApi.documentTypes.save(docType); + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @@ -47,9 +47,9 @@ test.describe('Modelsbuilder tests', () => { await umbracoUi.createContentWithDocumentType(docTypeName); await umbracoUi.setEditorHeaderName(contentName); // Fortunately for us the input field of a text box has the alias of the property as an id :) - await page.locator("#title").type("Hello world!") + await page.locator("#title").type("Hello world!"); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); - await umbracoUi.isSuccessNotificationVisible(); + await umbracoUi.isSuccessNotificationVisible({timeout:10000}); // Ensure that we can render it on the frontend = we can compile the models and views await umbracoApi.content.verifyRenderedContent("/", "

Hello world!

", true); @@ -62,7 +62,7 @@ test.describe('Modelsbuilder tests', () => { const docTypeName = "TestDocument"; const docTypeAlias = AliasHelper.toAlias(docTypeName); const propertyAlias = "title"; - const propertyValue = "Hello world!" + const propertyValue = "Hello world!"; await umbracoApi.content.deleteAllContent(); await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); @@ -80,8 +80,8 @@ test.describe('Modelsbuilder tests', () => { .done() .done() .build(); - const savedDocType = await umbracoApi.documentTypes.save(docType); + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @@ -90,7 +90,6 @@ test.describe('Modelsbuilder tests', () => { }

@Model.Title

`); - const content = new ContentBuilder() .withContentTypeAlias(savedDocType["alias"]) .withAction("publishNew") @@ -104,7 +103,6 @@ test.describe('Modelsbuilder tests', () => { .done() .done() .build() - await umbracoApi.content.save(content); // Navigate to the document type @@ -118,7 +116,8 @@ test.describe('Modelsbuilder tests', () => { await page.locator('.umb-card-grid >> [title="Textstring"]').click(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit)); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); - await umbracoUi.isSuccessNotificationVisible(); + // Has a long timeout because it can sometimes take longer than 5 sec to save on the pipeline + await umbracoUi.isSuccessNotificationVisible({timeout:10000}); // Now that the content is updated and the models are rebuilt, ensure that we can still render the frontend. await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

", true) @@ -132,7 +131,7 @@ test.describe('Modelsbuilder tests', () => { const docTypeName = "TestDocument"; const docTypeAlias = AliasHelper.toAlias(docTypeName); const propertyAlias = "title"; - const propertyValue = "Hello world!" + const propertyValue = "Hello world!"; await umbracoApi.content.deleteAllContent(); await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); @@ -150,8 +149,8 @@ test.describe('Modelsbuilder tests', () => { .done() .done() .build(); - const savedDocType = await umbracoApi.documentTypes.save(docType); + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @@ -173,8 +172,7 @@ test.describe('Modelsbuilder tests', () => { .withValue(propertyValue) .done() .done() - .build() - + .build(); await umbracoApi.content.save(content); // Navigate to the document type @@ -183,11 +181,12 @@ test.describe('Modelsbuilder tests', () => { const editor = await page.locator('.ace_content'); await editor.click(); // We only have to type out the opening tag, the editor adds the closing tag automatically. - await editor.type("

Edited") - await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)) + await editor.type("

Edited"); + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); + + await umbracoUi.isSuccessNotificationVisible({timeout:10000}); - await umbracoUi.isSuccessNotificationVisible(); - await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

Edited

", true) + await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

Edited

", true); await umbracoApi.content.deleteAllContent(); await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); @@ -198,7 +197,7 @@ test.describe('Modelsbuilder tests', () => { const docTypeName = "TestDocument"; const docTypeAlias = AliasHelper.toAlias(docTypeName); const propertyAlias = "title"; - const propertyValue = "Hello world!" + const propertyValue = "Hello world!"; const contentName = "Home"; await umbracoApi.content.deleteAllContent(); @@ -217,8 +216,8 @@ test.describe('Modelsbuilder tests', () => { .done() .done() .build(); - const savedDocType = await umbracoApi.documentTypes.save(docType); + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; @@ -240,8 +239,7 @@ test.describe('Modelsbuilder tests', () => { .withValue(propertyValue) .done() .done() - .build() - + .build(); await umbracoApi.content.save(content); // Navigate to the document type @@ -263,9 +261,9 @@ test.describe('Modelsbuilder tests', () => { const editor = await page.locator('.ace_content'); await editor.click(); // We only have to type out the opening tag, the editor adds the closing tag automatically. - await editor.type("

@Model.Bod") - await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)) - await umbracoUi.isSuccessNotificationVisible(); + await editor.type("

@Model.Bod"); + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); + await umbracoUi.isSuccessNotificationVisible({timeout: 10000}); await page.locator('span:has-text("×")').click(); // Navigate to the content section and update the content @@ -273,12 +271,12 @@ test.describe('Modelsbuilder tests', () => { await umbracoUi.refreshContentTree(); await umbracoUi.clickElement(umbracoUi.getTreeItem("content", [contentName])); await page.locator("#bod").type("Fancy body text"); - await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)) + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

Fancy body text

", true); await umbracoApi.content.deleteAllContent(); await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); - await umbracoApi.templates.ensureNameNotExists(docTypeName) + await umbracoApi.templates.ensureNameNotExists(docTypeName); }); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/packages.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/packages.spec.ts index 07bd1812e8..f08a48b8fd 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/packages.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/packages.spec.ts @@ -37,8 +37,8 @@ test.describe('Packages', () => { .withContentTypeAlias(rootDocTypeAlias) .withAction("saveNew") .addVariant() - .withName(nodeName) - .withSave(true) + .withName(nodeName) + .withSave(true) .done() .build(); const generatedContent = await umbracoApi.content.save(rootContentNode); @@ -116,8 +116,12 @@ test.describe('Packages', () => { // Navigate to create package section await umbracoUi.goToSection(ConstantHelper.sections.packages); await page.locator('[data-element="sub-view-umbCreatedPackages"]').click() - await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.delete)); - await page.waitForTimeout(100); + + // Selects the correct package + await page.locator('text=' + packageName + ' Delete >> button').click(); + + // Waits until the selector is visible + await expect(page.locator('[label-key="contentTypeEditor_yesDelete"]')).toBeVisible(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey('contentTypeEditor_yesDelete')); // Assert @@ -126,7 +130,6 @@ test.describe('Packages', () => { // Cleanup await umbracoApi.content.deleteAllContent(); await umbracoApi.documentTypes.ensureNameNotExists(rootDocTypeName); - await umbracoApi.packages.ensureNameNotExists(packageName); }); test('Download package', async ({page, umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/macro.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/macro.spec.ts index 11f99b3a95..8f9f8af071 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/macro.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/macro.spec.ts @@ -10,13 +10,13 @@ test.describe('Macros', () => { test('Create macro', async ({ page, umbracoApi, umbracoUi }) => { const name = "Test macro"; - const partialViewName = "Test partialView"; + const partialViewMacroFileName = "Test_partialView"; await umbracoApi.macros.ensureNameNotExists(name); - await umbracoApi.partialViews.ensureMacroFileNameNotExists(partialViewName); + await umbracoApi.partialViews.ensureMacroFileNameNotExists(partialViewMacroFileName + ".cshtml"); const partialViewMacro = new PartialViewMacroBuilder() - .withName(partialViewName) + .withName(partialViewMacroFileName) .withContent("@inherits Umbraco.Web.Macros.PartialViewMacroPage") .build(); await umbracoApi.partialViews.save(partialViewMacro); @@ -32,7 +32,7 @@ test.describe('Macros', () => { // Adds partial view to macro await page.locator('[data-element="property-label-macroPartialViewPickerProperty"]').click(); - await page.locator('[data-element="tree-item-' + partialViewName + '.cshtml"]').click(); + await page.locator('[data-element="tree-item-' + partialViewMacroFileName + '.cshtml"]').click(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); @@ -41,6 +41,6 @@ test.describe('Macros', () => { // Clean up await umbracoApi.macros.ensureNameNotExists(name); - await umbracoApi.partialViews.ensureMacroFileNameNotExists(partialViewName); + await umbracoApi.partialViews.ensureMacroFileNameNotExists(partialViewMacroFileName + ".cshtml"); }); }); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/templates.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/templates.spec.ts index bef67819d6..5e2adc6780 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/templates.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/templates.spec.ts @@ -3,10 +3,18 @@ import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; import {TemplateBuilder} from "@umbraco/json-models-builders"; test.describe('Templates', () => { + + const name = 'Templatetest'; + test.beforeEach(async ({ page, umbracoApi }, testInfo) => { await umbracoApi.report.report(testInfo); await umbracoApi.login(); + await umbracoApi.templates.ensureNameNotExists(name); }); + + test.afterEach(async ({page, umbracoApi}) => { + await umbracoApi.templates.ensureNameNotExists(name); + }) async function navigateToSettings(page, umbracoUi) { await umbracoUi.goToSection(ConstantHelper.sections.settings); @@ -15,67 +23,58 @@ test.describe('Templates', () => { async function createTemplate(page, umbracoUi) { await navigateToSettings(page, umbracoUi); - await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["Templates"]), {button: "right"}); + await umbracoUi.clickDataElementByElementName('tree-item-templates', {button: "right"}); await umbracoUi.clickElement(umbracoUi.getContextMenuAction(ConstantHelper.actions.create)); } test('Create template', async ({page, umbracoApi, umbracoUi}) => { - const name = "Create template test"; - await umbracoApi.templates.ensureNameNotExists(name); - await createTemplate(page, umbracoUi); // We have to wait for the ace editor to load, because when the editor is loading it will "steal" the focus briefly, // which causes the save event to fire if we've added something to the header field, causing errors. await page.waitForTimeout(500); - // Type name - await umbracoUi.getEditorHeaderName(name) + await umbracoUi.getEditorHeaderName(name); // Save // We must drop focus for the auto save event to occur. await page.focus('.btn-success'); // And then wait for the auto save event to finish by finding the page in the tree view. // This is a bit of a roundabout way to find items in a tree view since we dont use umbracoTreeItem // but we must be able to wait for the save event to finish, and we can't do that with umbracoTreeItem - const label = await page.locator("a:has-text('Create template test')"); - await expect(label).toBeVisible({timeout: 10000}); + await expect(page.locator('[data-element="tree-item-' + name + '"]')).toBeVisible({timeout: 10000}); // Now that the auto save event has finished we can save // and there wont be any duplicates or file in use errors. await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); - //Assert + + // Assert await umbracoUi.isSuccessNotificationVisible(); // For some reason cy.umbracoErrorNotification tries to click the element which is not possible // if it doesn't actually exist, making should('not.be.visible') impossible. await expect(await page.locator('.umb-notifications__notifications > .alert-error')).not.toBeVisible(); - - // Clean up - await umbracoApi.templates.ensureNameNotExists(name); }); test('Unsaved changes stay', async ({page, umbracoApi, umbracoUi}) => { - const name = "Templates Unsaved Changes Stay test"; const edit = "var num = 5;"; - await umbracoApi.templates.ensureNameNotExists(name); const template = new TemplateBuilder() .withName(name) .withContent('@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n') .build(); - await umbracoApi.templates.saveTemplate(template); - await navigateToSettings(page, umbracoUi); + await umbracoUi.navigateToTemplate(name); - // Open partial view - await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["Templates", name])); // Edit + // We select the template so it does not auto save. + await page.locator('.ace_content').click(); + await page.waitForTimeout(1000); await page.locator('.ace_content').type(edit); await expect(await page.locator('.ace_content')).toBeVisible(); await expect(await page.locator('.btn-success')).toBeVisible(); - + // Navigate away await umbracoUi.goToSection(ConstantHelper.sections.content); await umbracoUi.waitForTreeLoad(ConstantHelper.sections.content); - + // Click stay button await page.locator('umb-button[label="Stay"] button:enabled').click(); @@ -83,29 +82,23 @@ test.describe('Templates', () => { // That the same document is open await expect(await page.locator('#headerName')).toHaveValue(name); await expect(await page.locator('.ace_content')).toContainText(edit); - - // Clean up - await umbracoApi.templates.ensureNameNotExists(name); }); test('Discard unsaved changes', async ({page, umbracoApi, umbracoUi}) => { - const name = "Discard changes test"; const edit = "var num = 5;"; - await umbracoApi.templates.ensureNameNotExists(name); - const template = new TemplateBuilder() .withName(name) .withContent('@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\n') .build(); - await umbracoApi.templates.saveTemplate(template); - await navigateToSettings(page, umbracoUi); - - // Open partial view - await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["Templates", name])); + await umbracoUi.navigateToTemplate(name); + // Edit + // We select the template so it does not auto save. + await page.locator('.ace_content').click(); + await page.waitForTimeout(1000); await page.locator('.ace_content').type(edit); await expect(await page.locator('.ace_content')).toBeVisible(); await expect(await page.locator('.btn-success')).toBeVisible(); @@ -120,27 +113,20 @@ test.describe('Templates', () => { // Asserts await expect(await page.locator('.ace_content')).not.toContainText(edit); - // cy.umbracoPartialViewExists(fileName).then(exists => { expect(exists).to.be.false; }); TODO: Switch to template - await umbracoApi.templates.ensureNameNotExists(name); }); test('Insert macro', async ({page, umbracoApi, umbracoUi}) => { - const name = 'InsertMacroTest'; - - await umbracoApi.templates.ensureNameNotExists(name); await umbracoApi.macros.ensureNameNotExists(name); const template = new TemplateBuilder() .withName(name) .withContent('') .build(); - await umbracoApi.templates.saveTemplate(template); await umbracoApi.macros.save(name); - await navigateToSettings(page, umbracoUi); - await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["Templates", name])); + await umbracoUi.navigateToTemplate(name); // Insert macro await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.insert)); @@ -151,24 +137,17 @@ test.describe('Templates', () => { await expect(await page.locator('.ace_content')).toContainText('@await Umbraco.RenderMacroAsync("' + name + '")'); // Clean - await umbracoApi.templates.ensureNameNotExists(name); await umbracoApi.macros.ensureNameNotExists(name); - }); - + }); + test('Insert value', async ({page, umbracoApi, umbracoUi}) => { - const name = 'Insert Value Test'; - - await umbracoApi.templates.ensureNameNotExists(name); - const partialView = new TemplateBuilder() .withName(name) .withContent('') .build(); - await umbracoApi.templates.saveTemplate(partialView); - await navigateToSettings(page, umbracoUi); - await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["Templates", name])); + await umbracoUi.navigateToTemplate(name); // Insert value await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.insert)); @@ -177,10 +156,7 @@ test.describe('Templates', () => { await page.selectOption('select', 'umbracoBytes'); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit)); - // assert + // Assert await expect(await page.locator('.ace_content')).toContainText('@Model.Value("umbracoBytes")'); - - // Clean - await umbracoApi.templates.ensureNameNotExists(name); }); -}); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 6a96686b52..7792358c13 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs index 3cc54f4933..e18993ede1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs @@ -305,7 +305,7 @@ public class UdiTests } [UdiDefinition("foo", UdiType.GuidUdi)] - public class FooConnector : IServiceConnector + public class FooConnector : IServiceConnector2 { public IArtifact GetArtifact(Udi udi, IContextCache contextCache) => throw new NotImplementedException(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 1714bece2d..c5bc8d7c50 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -8,8 +8,8 @@ - - + +