diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index a33ee744f7..92621c30d3 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -432,6 +432,33 @@ stages: displayName: Start SQL Server Docker image (Linux) condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - powershell: | + $maxAttempts = 12 + $attempt = 0 + $status = "" + + while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) { + Start-Sleep -Seconds 5 + # We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build. + $status = docker inspect -f '{{.State.Status}}' mssql + + if ($status -ne 'running') { + Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)" + $attempt++ + } + } + + if ($status -eq 'running') { + Write-Host "SQL Server container is running" + docker ps -a + } else { + Write-Host "SQL Server did not become ready in time. Last known status: $status" + docker logs mssql + exit 1 + } + displayName: Wait for SQL Server to be ready (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + - pwsh: SqlLocalDB start MSSQLLocalDB displayName: Start SQL Server LocalDB (Windows) condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml index 9ff53d86d3..579ec9c908 100644 --- a/build/nightly-E2E-test-pipelines.yml +++ b/build/nightly-E2E-test-pipelines.yml @@ -300,17 +300,17 @@ stages: testCommand: "npm run testSqlite -- --shard=1/3" vmImage: "ubuntu-latest" SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) - CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True" + CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" LinuxPart2Of3: testCommand: "npm run testSqlite -- --shard=2/3" vmImage: "ubuntu-latest" SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) - CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True" + CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" LinuxPart3Of3: testCommand: "npm run testSqlite -- --shard=3/3" vmImage: "ubuntu-latest" SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD) - CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True" + CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True" WindowsPart1Of3: vmImage: "windows-latest" testCommand: "npm run testSqlite -- --shard=1/3" diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CopyDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CopyDocumentController.cs index 7300e6f88a..8d8c6fd57d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/CopyDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CopyDocumentController.cs @@ -1,4 +1,4 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -42,12 +42,16 @@ public class CopyDocumentController : DocumentControllerBase Guid id, CopyDocumentRequestModel copyDocumentRequestModel) { - AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + AuthorizationResult sourceAuthorizationResult = await _authorizationService.AuthorizeResourceAsync( User, - ContentPermissionResource.WithKeys(ActionCopy.ActionLetter, new[] { copyDocumentRequestModel.Target?.Id, id }), + ContentPermissionResource.WithKeys(ActionCopy.ActionLetter, [id]), + AuthorizationPolicies.ContentPermissionByResource); + AuthorizationResult destinationAuthorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionNew.ActionLetter, [copyDocumentRequestModel.Target?.Id]), AuthorizationPolicies.ContentPermissionByResource); - if (!authorizationResult.Succeeded) + if (sourceAuthorizationResult.Succeeded is false || destinationAuthorizationResult.Succeeded is false) { return Forbidden(); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs index 62e15d7e74..39fafe590b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs @@ -28,12 +28,13 @@ public class SearchDocumentItemController : DocumentItemControllerBase CancellationToken cancellationToken, string query, bool? trashed = null, + string? culture = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery] IEnumerable? allowedDocumentTypes = null) { - PagedModel searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take); + PagedModel searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, culture, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_documentPresentationFactory.CreateItemResponseModel), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs index aa777a57f0..1141b34b5c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/MoveDocumentController.cs @@ -1,4 +1,4 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -39,6 +39,20 @@ public class MoveDocumentController : DocumentControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Move(CancellationToken cancellationToken, Guid id, MoveDocumentRequestModel moveDocumentRequestModel) { + AuthorizationResult sourceAuthorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionMove.ActionLetter, [id]), + AuthorizationPolicies.ContentPermissionByResource); + AuthorizationResult destinationAuthorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionNew.ActionLetter, [moveDocumentRequestModel.Target?.Id]), + AuthorizationPolicies.ContentPermissionByResource); + + if (sourceAuthorizationResult.Succeeded is false || destinationAuthorizationResult.Succeeded is false) + { + return Forbidden(); + } + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( User, ContentPermissionResource.WithKeys(ActionMove.ActionLetter, new[] { moveDocumentRequestModel.Target?.Id, id }), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs index b5da421abe..d3202e1df1 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs @@ -21,17 +21,20 @@ public class SearchMediaItemController : MediaItemControllerBase _mediaPresentationFactory = mediaPresentationFactory; } - [NonAction] - [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] - public Task SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable? allowedMediaTypes = null) - => SearchFromParentWithAllowedTypes(cancellationToken, query, null, skip, take, parentId); - [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable? allowedMediaTypes = null) + public async Task SearchFromParentWithAllowedTypes( + CancellationToken cancellationToken, + string query, + bool? trashed = null, + string? culture = null, + int skip = 0, + int take = 100, + Guid? parentId = null, + [FromQuery]IEnumerable? allowedMediaTypes = null) { - PagedModel searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take); + PagedModel searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, culture, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_mediaPresentationFactory.CreateItemResponseModel), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Segment/AllSegmentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Segment/AllSegmentController.cs index 48542c6b31..c97e6c8d90 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Segment/AllSegmentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Segment/AllSegmentController.cs @@ -14,7 +14,6 @@ using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Segment; [ApiVersion("1.0")] -[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class AllSegmentController : SegmentControllerBase { private readonly ISegmentService _segmentService; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index 15dd835fa0..cff8182232 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.NotificationHandlers; using Umbraco.Cms.Api.Management.Security; using Umbraco.Cms.Api.Management.Services; using Umbraco.Cms.Api.Management.Telemetry; @@ -12,6 +13,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; @@ -74,6 +76,9 @@ public static partial class UmbracoBuilderExtensions services.AddScoped(); + // Register a notification handler to interrogate the registered external login providers at startup. + builder.AddNotificationHandler(); + return builder; } diff --git a/src/Umbraco.Cms.Api.Management/NotificationHandlers/ExternalLoginProviderStartupHandler.cs b/src/Umbraco.Cms.Api.Management/NotificationHandlers/ExternalLoginProviderStartupHandler.cs new file mode 100644 index 0000000000..b538f7a947 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/NotificationHandlers/ExternalLoginProviderStartupHandler.cs @@ -0,0 +1,44 @@ +using Umbraco.Cms.Api.Management.Security; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Api.Management.NotificationHandlers; + +/// +/// Invalidates backoffice sessions and clears external logins for removed providers if the external login +/// provider setup has changed. +/// +internal sealed class ExternalLoginProviderStartupHandler : INotificationHandler +{ + private readonly IBackOfficeExternalLoginProviders _backOfficeExternalLoginProviders; + private readonly IRuntimeState _runtimeState; + private readonly IServerRoleAccessor _serverRoleAccessor; + + /// + /// Initializes a new instance of the class. + /// + public ExternalLoginProviderStartupHandler( + IBackOfficeExternalLoginProviders backOfficeExternalLoginProviders, + IRuntimeState runtimeState, + IServerRoleAccessor serverRoleAccessor) + { + _backOfficeExternalLoginProviders = backOfficeExternalLoginProviders; + _runtimeState = runtimeState; + _serverRoleAccessor = serverRoleAccessor; + } + + /// + public void Handle(UmbracoApplicationStartingNotification notification) + { + if (_runtimeState.Level != RuntimeLevel.Run || + _serverRoleAccessor.CurrentServerRole == ServerRole.Subscriber) + { + return; + } + + _backOfficeExternalLoginProviders.InvalidateSessionsIfExternalLoginProvidersChanged(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 9d7a521046..2370516f51 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -10159,6 +10159,13 @@ "type": "boolean" } }, + { + "name": "culture", + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "skip", "in": "query", @@ -15918,6 +15925,13 @@ "type": "boolean" } }, + { + "name": "culture", + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "skip", "in": "query", diff --git a/src/Umbraco.Cms.Api.Management/Security/BackOfficeExternalLoginProviders.cs b/src/Umbraco.Cms.Api.Management/Security/BackOfficeExternalLoginProviders.cs index 2338dace96..7ecb178db5 100644 --- a/src/Umbraco.Cms.Api.Management/Security/BackOfficeExternalLoginProviders.cs +++ b/src/Umbraco.Cms.Api.Management/Security/BackOfficeExternalLoginProviders.cs @@ -1,5 +1,9 @@ using Microsoft.AspNetCore.Authentication; using Umbraco.Cms.Core.Security; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Security; @@ -8,13 +12,37 @@ public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProvider { private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; private readonly Dictionary _externalLogins; + private readonly IKeyValueService _keyValueService; + private readonly IExternalLoginWithKeyService _externalLoginWithKeyService; + private readonly ILogger _logger; + private const string ExternalLoginProvidersKey = "Umbraco.Cms.Web.BackOffice.Security.BackOfficeExternalLoginProviders"; + + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")] public BackOfficeExternalLoginProviders( IEnumerable externalLogins, IAuthenticationSchemeProvider authenticationSchemeProvider) + : this( + externalLogins, + authenticationSchemeProvider, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public BackOfficeExternalLoginProviders( + IEnumerable externalLogins, + IAuthenticationSchemeProvider authenticationSchemeProvider, + IKeyValueService keyValueService, + IExternalLoginWithKeyService externalLoginWithKeyService, + ILogger logger) { _externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType); _authenticationSchemeProvider = authenticationSchemeProvider; + _keyValueService = keyValueService; + _externalLoginWithKeyService = externalLoginWithKeyService; + _logger = logger; } /// @@ -60,4 +88,25 @@ public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProvider var found = _externalLogins.Values.Where(x => x.Options.DenyLocalLogin).ToList(); return found.Count > 0; } + + /// + public void InvalidateSessionsIfExternalLoginProvidersChanged() + { + var previousExternalLoginProvidersValue = _keyValueService.GetValue(ExternalLoginProvidersKey); + var currentExternalLoginProvidersValue = string.Join("|", _externalLogins.Keys.OrderBy(key => key)); + + if ((previousExternalLoginProvidersValue ?? string.Empty) != currentExternalLoginProvidersValue) + { + _logger.LogWarning( + "The configured external login providers have changed. Existing backoffice sessions using the removed providers will be invalidated and external login data removed."); + + _externalLoginWithKeyService.PurgeLoginsForRemovedProviders(_externalLogins.Keys); + + _keyValueService.SetValue(ExternalLoginProvidersKey, currentExternalLoginProvidersValue); + } + else if (previousExternalLoginProvidersValue is null) + { + _keyValueService.SetValue(ExternalLoginProvidersKey, string.Empty); + } + } } diff --git a/src/Umbraco.Cms.Api.Management/Security/IBackOfficeExternalLoginProviders.cs b/src/Umbraco.Cms.Api.Management/Security/IBackOfficeExternalLoginProviders.cs index 5dba04684c..2ff70a6015 100644 --- a/src/Umbraco.Cms.Api.Management/Security/IBackOfficeExternalLoginProviders.cs +++ b/src/Umbraco.Cms.Api.Management/Security/IBackOfficeExternalLoginProviders.cs @@ -23,4 +23,11 @@ public interface IBackOfficeExternalLoginProviders /// /// bool HasDenyLocalLogin(); + + /// + /// Used during startup to see if the configured external login providers is different from the persisted information. + /// If they are different, this will invalidate backoffice sessions and clear external logins for removed providers + /// if the external login provider setup has changed. + /// + void InvalidateSessionsIfExternalLoginProvidersChanged() { } } diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index d359f6d208..bfff570c4f 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -28,8 +28,8 @@ public class SecuritySettings internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60; internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60; - private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000; - private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250; + internal const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000; + internal const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250; internal const string StaticAuthorizeCallbackPathName = "/umbraco/oauth_complete"; internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout"; internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error"; diff --git a/src/Umbraco.Core/Extensions/IntExtensions.cs b/src/Umbraco.Core/Extensions/IntExtensions.cs index 6bdb3c6435..5b744e26e2 100644 --- a/src/Umbraco.Core/Extensions/IntExtensions.cs +++ b/src/Umbraco.Core/Extensions/IntExtensions.cs @@ -1,15 +1,17 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Diagnostics.CodeAnalysis; + namespace Umbraco.Extensions; public static class IntExtensions { /// - /// Does something 'x' amount of times + /// Does something 'x' amount of times. /// - /// - /// + /// Number of times to execute the action. + /// The action to execute. public static void Times(this int n, Action action) { for (var i = 0; i < n; i++) @@ -19,11 +21,11 @@ public static class IntExtensions } /// - /// Creates a Guid based on an integer value + /// Creates a Guid based on an integer value. /// - /// value to convert + /// The value to convert. /// - /// + /// The converted . /// public static Guid ToGuid(this int value) { @@ -31,4 +33,25 @@ public static class IntExtensions BitConverter.GetBytes(value).CopyTo(bytes); return new Guid(bytes); } + + /// + /// Restores a GUID previously created from an integer value using . + /// + /// The value to convert. + /// The converted . + /// + /// True if the value could be created, otherwise false. + /// + public static bool TryParseFromGuid(Guid value, [NotNullWhen(true)] out int? result) + { + if (value.ToString().EndsWith("-0000-0000-0000-000000000000") is false) + { + // We have a proper GUID, not one converted from an integer. + result = null; + return false; + } + + result = BitConverter.ToInt32(value.ToByteArray()); + return true; + } } diff --git a/src/Umbraco.Core/Extensions/StringExtensions.cs b/src/Umbraco.Core/Extensions/StringExtensions.cs index eab9d3fabf..7a3d90b205 100644 --- a/src/Umbraco.Core/Extensions/StringExtensions.cs +++ b/src/Umbraco.Core/Extensions/StringExtensions.cs @@ -1575,4 +1575,22 @@ public static class StringExtensions // this is by far the fastest way to find string needles in a string haystack public static int CountOccurrences(this string haystack, string needle) => haystack.Length - haystack.Replace(needle, string.Empty).Length; + + /// + /// Verifies the provided string is a valid culture code and returns it in a consistent casing. + /// + /// Culture code. + /// Culture code in standard casing. + public static string? EnsureCultureCode(this string? culture) + { + if (string.IsNullOrEmpty(culture) || culture == "*") + { + return culture; + } + + // Create as CultureInfo instance from provided name so we can ensure consistent casing of culture code when persisting. + // This will accept mixed case but once created have a `Name` property that is consistently and correctly cased. + // Will throw in an invalid culture code is provided. + return new CultureInfo(culture).Name; + } } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 768e631359..f9fa7263ba 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -1,5 +1,7 @@ -using System.Collections.Specialized; +using System.Collections.Specialized; using System.Diagnostics; +using System.Globalization; +using System.Runtime.ConstrainedExecution; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Extensions; @@ -288,6 +290,8 @@ public abstract class ContentBase : TreeEntityBase, IContentBase // set on variant content type if (ContentType.VariesByCulture()) { + culture = culture.EnsureCultureCode(); + // invariant is ok if (culture.IsNullOrWhiteSpace()) { @@ -297,7 +301,7 @@ public abstract class ContentBase : TreeEntityBase, IContentBase // clear else if (name.IsNullOrWhiteSpace()) { - ClearCultureInfo(culture!); + ClearCultureInfo(culture); } // set @@ -322,11 +326,6 @@ public abstract class ContentBase : TreeEntityBase, IContentBase private void ClearCultureInfo(string culture) { - if (culture == null) - { - throw new ArgumentNullException(nameof(culture)); - } - if (string.IsNullOrWhiteSpace(culture)) { throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); @@ -455,6 +454,7 @@ public abstract class ContentBase : TreeEntityBase, IContentBase $"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\"."); } + culture = culture.EnsureCultureCode(); var updated = property.SetValue(value, culture, segment); if (updated) { diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs index ec9a79530c..0329ceb33b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs @@ -3,23 +3,29 @@ using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Persistence.Repositories; /// -/// Repository for external logins with Guid as key, so it can be shared for members and users +/// Repository for external logins with Guid as key, so it can be shared for members and users. /// public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository, IQueryRepository { /// - /// Replaces all external login providers for the user/member key + /// Replaces all external login providers for the user/member key. /// void Save(Guid userOrMemberKey, IEnumerable logins); /// - /// Replaces all external login provider tokens for the providers specified for the user/member key + /// Replaces all external login provider tokens for the providers specified for the user/member key. /// void Save(Guid userOrMemberKey, IEnumerable tokens); /// - /// Deletes all external logins for the specified the user/member key + /// Deletes all external logins for the specified the user/member key. /// void DeleteUserLogins(Guid userOrMemberKey); + + /// + /// Deletes external logins that aren't associated with the current collection of providers. + /// + /// The names of the currently configured providers. + void DeleteUserLoginsForRemovedProviders(IEnumerable currentLoginProviders) { } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs index 0f9cc14f79..4b006ecb1c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs @@ -1,4 +1,4 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; @@ -160,4 +160,10 @@ public interface IUserRepository : IReadWriteQueryRepository bool RemoveClientId(int id, string clientId); IUser? GetByClientId(string clientId); + + /// + /// Invalidates sessions for users that aren't associated with the current collection of providers. + /// + /// The names of the currently configured providers. + void InvalidateSessionsForRemovedProviders(IEnumerable currentLoginProviders) { } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 888b1dfdf2..d11e08bad3 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -12,14 +15,27 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase _logger; + /// /// Initializes a new instance of the class. /// /// The items. + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")] public DataValueReferenceFactoryCollection(Func> items) - : base(items) + : this( + items, + StaticServiceProvider.Instance.GetRequiredService>()) { } + /// + /// Initializes a new instance of the class. + /// + /// The items. + /// The logger. + public DataValueReferenceFactoryCollection(Func> items, ILogger logger) + : base(items) => _logger = logger; + /// /// Gets all unique references from the specified properties. /// @@ -33,7 +49,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase(); // Group by property editor alias to avoid duplicate lookups and optimize value parsing - foreach (var propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values)) + foreach (IGrouping> propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values)) { if (!propertyEditors.TryGet(propertyValuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) { @@ -48,7 +64,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase public ISet GetReferences(IDataEditor dataEditor, IEnumerable values) => - GetReferencesEnumerable(dataEditor, values).ToHashSet(); - private IEnumerable GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable values) + GetReferencesEnumerable(dataEditor, values, null).ToHashSet(); + + private ISet GetReferences(IDataEditor dataEditor, IEnumerable values, string propertyEditorAlias) => + GetReferencesEnumerable(dataEditor, values, propertyEditorAlias).ToHashSet(); + + private IEnumerable GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable values, string? propertyEditorAlias) { // TODO: We will need to change this once we support tracking via variants/segments // for now, we are tracking values from ALL variants if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference) { - foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences)) + foreach (UmbracoEntityReference reference in GetReferencesFromPropertyValues(values, dataValueReference, propertyEditorAlias)) { yield return reference; } @@ -107,6 +127,38 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase GetReferencesFromPropertyValues(IEnumerable values, IDataValueReference dataValueReference, string? propertyEditorAlias) + { + var result = new List(); + foreach (var value in values) + { + // When property editors on data types are changed, we could have values that are incompatible with the new editor. + // Leading to issues such as: + // - https://github.com/umbraco/Umbraco-CMS/issues/17628 + // - https://github.com/umbraco/Umbraco-CMS/issues/17725 + // Although some changes like this are not intended to be compatible, we should handle them gracefully and not + // error in retrieving references, which would prevent manipulating or deleting the content that uses the data type. + try + { + IEnumerable references = dataValueReference.GetReferences(value); + result.AddRange(references); + } + catch (Exception ex) + { + // Log the exception but don't throw, continue with the next value. + _logger.LogError( + ex, + "Error getting references from value {Value} with data editor {DataEditor} and property editor alias {PropertyEditorAlias}.", + value, + dataValueReference.GetType().FullName, + propertyEditorAlias ?? "n/a"); + throw; + } + } + + return result; + } + /// /// Gets all relation type aliases that are automatically tracked. /// @@ -114,6 +166,10 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase /// All relation type aliases that are automatically tracked. /// + [Obsolete("Use GetAllAutomaticRelationTypesAliases. This will be removed in Umbraco 15.")] + public ISet GetAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) => + GetAllAutomaticRelationTypesAliases(propertyEditors); + public ISet GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) { // Always add default automatic relation types diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/AuditService.cs index 6af3d15675..ee11be4341 100644 --- a/src/Umbraco.Core/Services/AuditService.cs +++ b/src/Umbraco.Core/Services/AuditService.cs @@ -264,7 +264,16 @@ public sealed class AuditService : RepositoryService, IAuditService } /// - public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails) + [Obsolete("Will be moved to a new service in V17. Scheduled for removal in V18.")] + public IAuditEntry Write( + int performingUserId, + string perfomingDetails, + string performingIp, + DateTime eventDateUtc, + int affectedUserId, + string? affectedDetails, + string eventType, + string eventDetails) { if (performingUserId < 0 && performingUserId != Constants.Security.SuperUserId) { diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index c72218e8f3..f246b9c3f0 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1184,6 +1184,8 @@ public class ContentService : RepositoryService, IContentService throw new ArgumentException("Cultures cannot be null or whitespace", nameof(cultures)); } + cultures = cultures.Select(x => x.EnsureCultureCode()!).ToArray(); + EventMessages evtMsgs = EventMessagesFactory.Get(); // we need to guard against unsaved changes before proceeding; the content will be saved, but we're not firing any saved notifications @@ -1263,7 +1265,7 @@ public class ContentService : RepositoryService, IContentService EventMessages evtMsgs = EventMessagesFactory.Get(); - culture = culture?.NullOrWhiteSpaceAsNull(); + culture = culture?.NullOrWhiteSpaceAsNull().EnsureCultureCode(); PublishedState publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) @@ -2063,7 +2065,7 @@ public class ContentService : RepositoryService, IContentService cultures = ["*"]; } - return cultures; + return cultures.Select(x => x.EnsureCultureCode()!).ToArray(); } private static bool ProvidedCulturesIndicatePublishAll(string[] cultures) => cultures.Length == 0 || (cultures.Length == 1 && cultures[0] == "invariant"); diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs index be49927b36..df64e2e355 100644 --- a/src/Umbraco.Core/Services/ExternalLoginService.cs +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -5,21 +5,40 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; -using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; public class ExternalLoginService : RepositoryService, IExternalLoginWithKeyService { private readonly IExternalLoginWithKeyRepository _externalLoginRepository; + private readonly IUserRepository _userRepository; + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")] public ExternalLoginService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IExternalLoginWithKeyRepository externalLoginRepository) - : base(provider, loggerFactory, eventMessagesFactory) => + : this( + provider, + loggerFactory, + eventMessagesFactory, + externalLoginRepository, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public ExternalLoginService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IExternalLoginWithKeyRepository externalLoginRepository, + IUserRepository userRepository) + : base(provider, loggerFactory, eventMessagesFactory) + { _externalLoginRepository = externalLoginRepository; + _userRepository = userRepository; + } public IEnumerable Find(string loginProvider, string providerKey) { @@ -80,4 +99,15 @@ public class ExternalLoginService : RepositoryService, IExternalLoginWithKeyServ scope.Complete(); } } + + /// + public void PurgeLoginsForRemovedProviders(IEnumerable currentLoginProviders) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + _userRepository.InvalidateSessionsForRemovedProviders(currentLoginProviders); + _externalLoginRepository.DeleteUserLoginsForRemovedProviders(currentLoginProviders); + scope.Complete(); + } + } } diff --git a/src/Umbraco.Core/Services/IAuditService.cs b/src/Umbraco.Core/Services/IAuditService.cs index 7481a5e613..4bd7b00023 100644 --- a/src/Umbraco.Core/Services/IAuditService.cs +++ b/src/Umbraco.Core/Services/IAuditService.cs @@ -144,6 +144,7 @@ public interface IAuditService : IService /// /// /// Free-form details about the audited event. + [Obsolete("Will be moved to a new service in V17. Scheduled for removal in V18.")] IAuditEntry Write( int performingUserId, string perfomingDetails, diff --git a/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs index 42f0708aaa..deaa135f16 100644 --- a/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs +++ b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs @@ -5,47 +5,53 @@ namespace Umbraco.Cms.Core.Services; public interface IExternalLoginWithKeyService : IService { /// - /// Returns all user logins assigned + /// Returns all user logins assigned. /// IEnumerable GetExternalLogins(Guid userOrMemberKey); /// - /// Returns all user login tokens assigned + /// Returns all user login tokens assigned. /// IEnumerable GetExternalLoginTokens(Guid userOrMemberKey); /// /// Returns all logins matching the login info - generally there should only be one but in some cases - /// there might be more than one depending on if an administrator has been editing/removing members + /// there might be more than one depending on if an administrator has been editing/removing members. /// IEnumerable Find(string loginProvider, string providerKey); /// - /// Saves the external logins associated with the user + /// Saves the external logins associated with the user. /// /// - /// The user or member key associated with the logins + /// The user or member key associated with the logins. /// /// /// - /// This will replace all external login provider information for the user + /// This will replace all external login provider information for the user. /// void Save(Guid userOrMemberKey, IEnumerable logins); /// - /// Saves the external login tokens associated with the user + /// Saves the external login tokens associated with the user. /// /// - /// The user or member key associated with the logins + /// The user or member key associated with the logins. /// /// /// - /// This will replace all external login tokens for the user + /// This will replace all external login tokens for the user. /// void Save(Guid userOrMemberKey, IEnumerable tokens); /// - /// Deletes all user logins - normally used when a member is deleted + /// Deletes all user logins - normally used when a member is deleted. /// void DeleteUserLogins(Guid userOrMemberKey); + + /// + /// Deletes external logins and invalidates sessions for users that aren't associated with the current collection of providers. + /// + /// The names of the currently configured providers. + void PurgeLoginsForRemovedProviders(IEnumerable currentLoginProviders) { } } diff --git a/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs b/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs index 06ff99c35c..4063c6ad87 100644 --- a/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs +++ b/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs @@ -35,7 +35,7 @@ public interface IIndexedEntitySearchService int skip = 0, int take = 100, bool ignoreUserStartNodes = false) - => Search(objectType,query, skip, take, ignoreUserStartNodes); + => Search(objectType, query, skip, take, ignoreUserStartNodes); Task> SearchAsync( UmbracoObjectTypes objectType, @@ -43,6 +43,7 @@ public interface IIndexedEntitySearchService Guid? parentId, IEnumerable? contentTypeIds, bool? trashed, + string? culture = null, int skip = 0, int take = 100, bool ignoreUserStartNodes = false); diff --git a/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs index aa0e9a09bf..83098ed5ab 100644 --- a/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs @@ -7,7 +7,11 @@ namespace Umbraco.Cms.Core.Templates; public sealed class HtmlImageSourceParser { private static readonly Regex ResolveImgPattern = new( - @"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", + @"]*(data-udi=""([^""]*)"")[^>]*>", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private static readonly Regex SrcAttributeRegex = new( + @"src=""([^""\?]*)(\?[^""]*)?""", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); private static readonly Regex DataUdiAttributeRegex = new( @@ -61,17 +65,26 @@ public sealed class HtmlImageSourceParser return ResolveImgPattern.Replace(text, match => { // match groups: - // - 1 = from the beginning of the image tag until src attribute value begins - // - 2 = the src attribute value excluding the querystring (if present) - // - 3 = anything after group 2 and before the data-udi attribute value begins - // - 4 = the data-udi attribute value - // - 5 = anything after group 4 until the image tag is closed - var udi = match.Groups[4].Value; + // - 1 = the data-udi attribute + // - 2 = the data-udi attribute value + var udi = match.Groups[2].Value; if (udi.IsNullOrWhiteSpace() || UdiParser.TryParse(udi, out GuidUdi? guidUdi) == false) { return match.Value; } + // Find the src attribute + // src match groups: + // - 1 = the src attribute value until the query string + // - 2 = the src attribute query string including the '?' + Match src = SrcAttributeRegex.Match(match.Value); + + if (src.Success == false) + { + // the src attribute isn't found, return the original value + return match.Value; + } + var mediaUrl = _getMediaUrl(guidUdi.Guid); if (mediaUrl == null) { @@ -80,7 +93,9 @@ public sealed class HtmlImageSourceParser return match.Value; } - return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; + var newImgTag = match.Value.Replace(src.Value, $"src=\"{mediaUrl}{src.Groups[2].Value}\""); + + return newImgTag; }); } @@ -91,6 +106,16 @@ public sealed class HtmlImageSourceParser /// public string RemoveImageSources(string text) - // see comment in ResolveMediaFromTextString for group reference - => ResolveImgPattern.Replace(text, "$1$3$4$5"); + // find each ResolveImgPattern match in the text, then find each + // SrcAttributeRegex match in the match value, then replace the src + // attribute value with an empty string + // (see comment in ResolveMediaFromTextString for group reference) + => ResolveImgPattern.Replace(text, match => + { + // Find the src attribute + Match src = SrcAttributeRegex.Match(match.Value); + + return src.Success == false || string.IsNullOrWhiteSpace(src.Groups[1].Value) ? + match.Value : match.Value.Replace(src.Groups[1].Value, string.Empty); + }); } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index aee4fdac67..0e4e6fdb42 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -248,6 +248,8 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + return builder; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 2c0f0f8c2d..766d654c08 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -56,6 +56,12 @@ internal class ExternalLoginRepository : EntityRepositoryBase Database.Delete("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey }); + /// + public void DeleteUserLoginsForRemovedProviders(IEnumerable currentLoginProviders) => + Database.Execute(Sql() + .Delete() + .WhereNotIn(x => x.LoginProvider, currentLoginProviders)); + /// public void Save(Guid userOrMemberKey, IEnumerable logins) { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index a1d5a41bea..187d5c5ce2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -12,7 +12,6 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -21,7 +20,6 @@ using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; /// @@ -1268,5 +1266,32 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 return sql; } + + /// + public void InvalidateSessionsForRemovedProviders(IEnumerable currentLoginProviders) + { + // Get all the user or member keys associated with the removed providers. + Sql idsQuery = SqlContext.Sql() + .Select(x => x.UserOrMemberKey) + .From() + .WhereNotIn(x => x.LoginProvider, currentLoginProviders); + List userAndMemberKeysAssociatedWithRemovedProviders = Database.Fetch(idsQuery); + if (userAndMemberKeysAssociatedWithRemovedProviders.Count == 0) + { + return; + } + + // Invalidate the security stamps on the users associated with the removed providers. + Sql updateSecurityStampsQuery = Sql() + .Update(u => u.Set(x => x.SecurityStampToken, "0".PadLeft(32, '0'))) + .WhereIn(x => x.Key, userAndMemberKeysAssociatedWithRemovedProviders); + Database.Execute(updateSecurityStampsQuery); + + // Delete the OpenIddict tokens for the users associated with the removed providers. + // The following is safe from SQL injection as we are dealing with GUIDs, not strings. + var userKeysForInClause = string.Join("','", userAndMemberKeysAssociatedWithRemovedProviders.Select(x => x.ToString())); + Database.Execute("DELETE FROM umbracoOpenIddictTokens WHERE Subject IN ('" + userKeysForInClause + "')"); + } + #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index a8fa01de69..3777d84eb4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -71,17 +71,17 @@ public abstract class BlockValuePropertyValueEditorBase : DataV continue; } - var districtValues = valuesByPropertyEditorAlias.Distinct().ToArray(); + var distinctValues = valuesByPropertyEditorAlias.Distinct().ToArray(); if (dataEditor.GetValueEditor() is IDataValueReference reference) { - foreach (UmbracoEntityReference value in districtValues.SelectMany(reference.GetReferences)) + foreach (UmbracoEntityReference value in distinctValues.SelectMany(reference.GetReferences)) { result.Add(value); } } - IEnumerable references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, districtValues); + IEnumerable references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, distinctValues); foreach (UmbracoEntityReference value in references) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 92c0a05e94..7d75725ef6 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -93,6 +93,7 @@ public class RichTextPropertyEditor : DataEditor private readonly RichTextEditorPastedImages _pastedImages; private readonly IJsonSerializer _jsonSerializer; private readonly IRichTextRequiredValidator _richTextRequiredValidator; + private readonly IRichTextRegexValidator _richTextRegexValidator; private readonly ILogger _logger; public RichTextPropertyValueEditor( @@ -111,6 +112,7 @@ public class RichTextPropertyEditor : DataEditor IPropertyValidationService propertyValidationService, DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection, IRichTextRequiredValidator richTextRequiredValidator, + IRichTextRegexValidator richTextRegexValidator, BlockEditorVarianceHandler blockEditorVarianceHandler, ILanguageService languageService, IIOHelper ioHelper) @@ -122,6 +124,7 @@ public class RichTextPropertyEditor : DataEditor _pastedImages = pastedImages; _htmlSanitizer = htmlSanitizer; _richTextRequiredValidator = richTextRequiredValidator; + _richTextRegexValidator = richTextRegexValidator; _jsonSerializer = jsonSerializer; _logger = logger; @@ -131,6 +134,8 @@ public class RichTextPropertyEditor : DataEditor public override IValueRequiredValidator RequiredValidator => _richTextRequiredValidator; + public override IValueFormatValidator FormatValidator => _richTextRegexValidator; + protected override RichTextBlockValue CreateWithLayout(IEnumerable layout) => new(layout); /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/Validators/IRichTextRegexValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/Validators/IRichTextRegexValidator.cs new file mode 100644 index 0000000000..8b69554322 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/Validators/IRichTextRegexValidator.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +internal interface IRichTextRegexValidator : IValueFormatValidator +{ +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/Validators/RichTextRegexValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/Validators/RichTextRegexValidator.cs new file mode 100644 index 0000000000..4e6a5c049e --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/Validators/RichTextRegexValidator.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +internal class RichTextRegexValidator : IRichTextRegexValidator +{ + private readonly RegexValidator _regexValidator; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + + public RichTextRegexValidator( + IJsonSerializer jsonSerializer, + ILogger logger) + { + _jsonSerializer = jsonSerializer; + _logger = logger; + _regexValidator = new RegexValidator(); + } + + public IEnumerable ValidateFormat(object? value, string? valueType, string format) => _regexValidator.ValidateFormat(GetValue(value), valueType, format); + + private object? GetValue(object? value) => + RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue) + ? richTextEditorValue?.Markup + : value; +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs b/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs index 2febcc68c0..c72771ba67 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs @@ -73,7 +73,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService int skip = 0, int take = 100, bool ignoreUserStartNodes = false) - => SearchAsync(objectType, query, parentId, contentTypeIds, trashed, skip, take, ignoreUserStartNodes).GetAwaiter().GetResult(); + => SearchAsync(objectType, query, parentId, contentTypeIds, trashed, null, skip, take, ignoreUserStartNodes).GetAwaiter().GetResult(); public Task> SearchAsync( UmbracoObjectTypes objectType, @@ -81,6 +81,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService Guid? parentId, IEnumerable? contentTypeIds, bool? trashed, + string? culture = null, int skip = 0, int take = 100, bool ignoreUserStartNodes = false) diff --git a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs index cd82fa6c8b..3d46df8f01 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs @@ -326,8 +326,9 @@ public class PackagingService : IPackagingService PackageName = group.Key.PackageName, }; + var packageKey = Constants.Conventions.Migrations.KeyValuePrefix + (group.Key.PackageId ?? group.Key.PackageName); var currentState = keyValues? - .GetValueOrDefault(Constants.Conventions.Migrations.KeyValuePrefix + group.Key.PackageId); + .GetValueOrDefault(packageKey); package.PackageMigrationPlans = group .Select(plan => new InstalledPackageMigrationPlans diff --git a/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs index e214ba1c23..01f7d7cfb9 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs @@ -31,7 +31,7 @@ public class ConfigureSecurityStampOptions : IConfigureOptions div { + position: relative; + display: block; + z-index: 10000; + } + .align-center { text-align: center; } diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts index ece6df083a..07abe03401 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts @@ -62,8 +62,9 @@ export default { unlock: 'Lås op', createblueprint: 'Opret indholdsskabelon', resendInvite: 'Gensend Invitation', - editContent: 'Edit content', - chooseWhereToImport: 'Choose where to import', + editContent: 'Rediger indhold', + chooseWhereToImport: 'Vælg hvor du vil importere', + viewActionsFor: (name) => (name ? `Se handlinger for '${name}'` : 'Se handlinger'), }, actionCategories: { content: 'Indhold', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 4c416894e3..f7a62dc346 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -72,6 +72,7 @@ export default { wasCopiedTo: 'was copied to', wasDeleted: 'was deleted', wasMovedTo: 'was moved to', + viewActionsFor: (name) => (name ? `View actions for '${name}'` : 'View actions'), }, actionCategories: { content: 'Content', @@ -159,7 +160,7 @@ export default { saveAndPublish: 'Save and publish', saveToPublish: 'Save and send for approval', saveListView: 'Save list view', - schedulePublish: 'Schedule', + schedulePublish: 'Schedule publish', saveAndPreview: 'Save and preview', showPageDisabled: "Preview is disabled because there's no template assigned", styleChoose: 'Choose style', @@ -271,7 +272,7 @@ export default { publishedPendingChanges: 'Published (pending changes)', publishStatus: 'Publication Status', publishDescendantsHelp: - 'Publish %0% and all content items underneath and thereby making their content publicly available.', + 'Publish %0% and all items underneath and thereby making their content publicly available.', publishDescendantsWithVariantsHelp: 'Publish variants and variants of same type underneath and thereby making their content publicly available.', noVariantsToProcess: 'There are no available variants', @@ -319,7 +320,7 @@ export default { addTextBox: 'Add another text box', removeTextBox: 'Remove this text box', contentRoot: 'Content root', - includeUnpublished: 'Include unpublished content items.', + includeUnpublished: 'Include unpublished items.', isSensitiveValue: 'This value is hidden. If you need access to view this value please contact your website administrator.', isSensitiveValue_short: 'This value is hidden.', @@ -348,6 +349,8 @@ export default { variantUnpublishNotAllowed: 'Unpublish is not allowed', selectAllVariants: 'Select all variants', saveModalTitle: 'Save', + saveAndPublishModalTitle: 'Save and publish', + publishModalTitle: 'Publish', }, blueprints: { createBlueprintFrom: "Create a new Document Blueprint from '%0%'", @@ -475,10 +478,11 @@ export default { discardChanges: 'Discard changes', unsavedChanges: 'Discard unsaved changes', unsavedChangesWarning: 'Are you sure you want to navigate away from this page? You have unsaved changes', - confirmListViewPublish: 'Publishing will make the selected items visible on the site.', - confirmListViewUnpublish: 'Unpublishing will remove the selected items and all their descendants from the site.', - confirmPublish: 'Publishing will make this page and all its published descendants visible on the site.', - confirmUnpublish: 'Unpublishing will remove this page and all its descendants from the site.', + confirmListViewPublish: 'Publishing will make the selected items publicly available.', + confirmListViewUnpublish: + 'Unpublishing will make the selected items and all their descendants publicly unavailable.', + confirmPublish: 'Publishing will make this content and all its published descendants publicly available.', + confirmUnpublish: 'Unpublishing will make this content publicly unavailable.', doctypeChangeWarning: 'You have unsaved changes. Making changes to the Document Type will discard the changes.', }, bulk: { @@ -911,7 +915,7 @@ export default { retrieve: 'Retrieve', retry: 'Retry', rights: 'Permissions', - scheduledPublishing: 'Scheduled Publishing', + scheduledPublishing: 'Schedule publish', umbracoInfo: 'Umbraco info', search: 'Search', searchNoResult: 'Sorry, we can not find what you are looking for.', @@ -2795,7 +2799,7 @@ export default { modalSource: 'Source', modalManual: 'Manual', modalAnchorValidationMessage: - 'Please enter an anchor or querystring, or select a published document or media item, or manually configure the URL.', + 'Please enter an anchor or querystring, select a document or media item, or manually configure the URL.', resetUrlHeadline: 'Reset URL?', resetUrlMessage: 'Are you sure you want to reset this URL?', resetUrlLabel: 'Reset', diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts index 3fdf1b10b0..a057b8bb15 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts @@ -84,7 +84,7 @@ export class UmbArrayState extends UmbDeepState { /** * @function remove - * @param {unknown[]} uniques - The unique values to remove. + * @param {U[]} uniques - The unique values to remove. * @returns {UmbArrayState} Reference to it self. * @description - Remove some new data of this Subject. * @example Example remove entry with id '1' and '2' @@ -95,7 +95,7 @@ export class UmbArrayState extends UmbDeepState { * const myState = new UmbArrayState(data, (x) => x.id); * myState.remove([1, 2]); */ - remove(uniques: unknown[]) { + remove(uniques: U[]) { if (this.getUniqueMethod) { let next = this.getValue(); if (!next) return this; @@ -114,7 +114,7 @@ export class UmbArrayState extends UmbDeepState { /** * @function removeOne - * @param {unknown} unique - The unique value to remove. + * @param {U} unique - The unique value to remove. * @returns {UmbArrayState} Reference to it self. * @description - Remove some new data of this Subject. * @example Example remove entry with id '1' @@ -125,7 +125,7 @@ export class UmbArrayState extends UmbDeepState { * const myState = new UmbArrayState(data, (x) => x.id); * myState.removeOne(1); */ - removeOne(unique: unknown) { + removeOne(unique: U) { if (this.getUniqueMethod) { let next = this.getValue(); if (!next) return this; @@ -251,7 +251,7 @@ export class UmbArrayState extends UmbDeepState { /** * @function updateOne - * @param {unknown} unique - Unique value to find entry to update. + * @param {U} unique - Unique value to find entry to update. * @param {Partial} entry - new data to be added in this Subject. * @returns {UmbArrayState} Reference to it self. * @description - Update a item with some new data, requires the ArrayState to be constructed with a getUnique method. @@ -263,7 +263,7 @@ export class UmbArrayState extends UmbDeepState { * const myState = new UmbArrayState(data, (x) => x.key); * myState.updateOne(2, {value: 'updated-bar'}); */ - updateOne(unique: unknown, entry: Partial) { + updateOne(unique: U, entry: Partial) { if (!this.getUniqueMethod) { throw new Error("Can't partial update an ArrayState without a getUnique method provided when constructed."); } 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 1f30e8298e..f0f5a9de01 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 @@ -427,6 +427,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper #extensionSlotRenderMethod = (ext: UmbExtensionElementInitializer) => { if (ext.component) { ext.component.classList.add('umb-block-grid__block--view'); + ext.component.setAttribute('part', 'component'); } if (this._exposed) { return ext.component; @@ -641,6 +642,11 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper border-color: var(--uui-color-invalid); } + umb-extension-slot::part(component) { + position: relative; + z-index: 0; + } + #invalidLocation { position: absolute; top: -1em; 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 805a2ef48c..089f332cd1 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 @@ -346,6 +346,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper }; #extensionSlotRenderMethod = (ext: UmbExtensionElementInitializer) => { + ext.component?.setAttribute('part', 'component'); if (this._exposed) { return ext.component; } else { @@ -511,6 +512,11 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper border-color: var(--uui-color-invalid); } + umb-extension-slot::part(component) { + position: relative; + z-index: 0; + } + uui-action-bar { position: absolute; top: var(--uui-size-2); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts index a056c7045f..6970c554e2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts @@ -243,6 +243,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert }; #extensionSlotRenderMethod = (ext: UmbExtensionElementInitializer) => { + ext.component?.setAttribute('part', 'component'); if (this._exposed) { return ext.component; } else { @@ -345,6 +346,12 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert outline: 3px solid var(--uui-color-focus); } } + + umb-extension-slot::part(component) { + position: relative; + z-index: 0; + } + uui-action-bar { position: absolute; top: var(--uui-size-2); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-properties.element.ts index 801babf652..1ce6295ebc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-properties.element.ts @@ -39,7 +39,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { _dataOwner?: UmbBlockElementManager; @state() - _variantId?: UmbVariantId; + _workspaceVariantId?: UmbVariantId; @state() _visibleProperties?: Array; @@ -56,7 +56,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { this.observe( workspaceContext?.variantId, (variantId) => { - this._variantId = variantId; + this._workspaceVariantId = variantId; this.#processPropertyStructure(); }, 'observeVariantId', @@ -83,16 +83,19 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { } #processPropertyStructure() { - if (!this._dataOwner || !this.#properties || !this.#propertyStructureHelper) { + if (!this._dataOwner || !this.#properties || !this.#propertyStructureHelper || !this._workspaceVariantId) { return; } const propertyViewGuard = this._dataOwner.propertyViewGuard; this.#properties.forEach((property) => { - const propertyVariantId = new UmbVariantId(this._variantId?.culture, this._variantId?.segment); + const propertyVariantId = new UmbVariantId( + property.variesByCulture ? this._workspaceVariantId!.culture : null, + property.variesBySegment ? this._workspaceVariantId!.segment : null, + ); this.observe( - propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property), + propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property, this._workspaceVariantId!), (permitted) => { if (permitted) { this.#visiblePropertiesUniques.push(property.unique); @@ -117,7 +120,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { } override render() { - return this._variantId && this._visibleProperties + return this._workspaceVariantId && this._visibleProperties ? repeat( this._visibleProperties, (property) => property.alias, @@ -126,7 +129,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { class="property" .ownerContext=${this._dataOwner} .ownerEntityType=${this._ownerEntityType} - .variantId=${this._variantId} + .variantId=${this._workspaceVariantId} .property=${property}>`, ) : nothing; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-property.element.ts index 20ea00482b..fb592580eb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-property.element.ts @@ -38,7 +38,11 @@ export class UmbBlockWorkspaceViewEditPropertyElement extends UmbLitElement { })}].value`; this.observe( - this.ownerContext.propertyWriteGuard.isPermittedForVariantAndProperty(propertyVariantId, this.property), + this.ownerContext.propertyWriteGuard.isPermittedForVariantAndProperty( + propertyVariantId, + this.property, + this.variantId, + ), (write) => { this._writeable = write; }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts index 0f0185994a..c5c98d5f1a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts @@ -150,7 +150,7 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement { slot="actions" .entityType=${item.entityType} .unique=${item.unique} - .label=${item.name}> + .label=${this.localize.term('actions_viewActionsFor', [item.name])}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.element.ts index e0cf37273d..7ed7dddb7a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.element.ts @@ -39,6 +39,9 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< @state() private _usedForInheritance: Array = []; + @state() + private _usedForComposition: Array = []; + override connectedCallback() { super.connectedCallback(); @@ -53,6 +56,7 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< this._selection = this.data?.selection ?? []; this._usedForInheritance = this.data?.usedForInheritance ?? []; + this._usedForComposition = this.data?.usedForComposition ?? []; this.modalContext?.setValue({ selection: this._selection }); const isNew = this.data!.isNew; @@ -131,7 +135,9 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< override render() { return html` - ${this._references.length ? this.#renderHasReference() : this.#renderAvailableCompositions()} + + ${this._references.length ? this.#renderHasReference() : this.#renderAvailableCompositions()} +
${!this._references.length @@ -213,11 +219,16 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< (compositions) => compositions.unique, (compositions) => { const usedForInheritance = this._usedForInheritance.includes(compositions.unique); + const usedForComposition = this._usedForComposition.includes(compositions.unique); + /* The server will return isCompatible as false if the Doc Type is currently being used in a composition. + Therefore, we need to account for this in the "isDisabled" check to ensure it remains enabled. + Otherwise, it would become disabled and couldn't be deselected by the user. */ + const isDisabled = usedForInheritance || (compositions.isCompatible === false && !usedForComposition); return html` this.#onSelectionAdd(compositions.unique)} @deselected=${() => this.#onSelectionRemove(compositions.unique)} ?selected=${this._selection.find((unique) => unique === compositions.unique)}> @@ -251,6 +262,10 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< align-items: center; gap: var(--uui-size-3); } + + .compositions-list { + margin-block: var(--uui-size-3); + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.token.ts index a711e4e4df..994d2d1912 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.token.ts @@ -5,6 +5,7 @@ export interface UmbCompositionPickerModalData { compositionRepositoryAlias: string; selection: Array; usedForInheritance: Array; + usedForComposition: Array; unique: string | null; isElement: boolean; currentPropertyAliases: Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts index 2dbe7595c0..58afbe2cb0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts @@ -194,6 +194,7 @@ export class UmbContentTypeContainerStructureHelper x?.variesByCulture); readonly variesBySegment = createObservablePart(this.ownerContentType, (x) => x?.variesBySegment); - #containers: UmbArrayState = new UmbArrayState( - [], - (x) => x.id, - ); containerById(id: string) { - return this.#containers.asObservablePart((x) => x.find((y) => y.id === id)); + return createObservablePart(this.#contentTypeContainers, (x) => x.find((y) => y.id === id)); } constructor(host: UmbControllerHost, typeRepository: UmbDetailRepository | string) { @@ -143,11 +140,20 @@ export class UmbContentTypeStructureManager< this.#repoManager.entries, (entries) => { // Prevent updating once that are have edited here. - entries = entries.filter( + const entriesToBeUpdated = entries.filter( (x) => !(this.#editedTypes.getHasOne(x.unique) && this.#contentTypes.getHasOne(x.unique)), ); - this.#contentTypes.append(entries); + // Remove entries based on no-longer existing uniques: + const entriesToBeRemoved = this.#contentTypes + .getValue() + .filter((entry) => !entries.some((x) => x.unique === entry.unique)) + .map((x) => x.unique); + + this.#contentTypes.mute(); + this.#contentTypes.remove(entriesToBeRemoved); + this.#contentTypes.append(entriesToBeUpdated); + this.#contentTypes.unmute(); }, null, ); @@ -161,13 +167,6 @@ export class UmbContentTypeStructureManager< }, null, ); - this.observe( - this.#contentTypeContainers, - (contentTypeContainers) => { - this.#containers.setValue(contentTypeContainers); - }, - null, - ); } /** @@ -266,6 +265,8 @@ export class UmbContentTypeStructureManager< } async #loadContentTypeCompositions(contentTypeCompositions: T['compositions'] | undefined) { + // Important to wait a JS-cycle, cause this is called by an observation of a state and this results in setting the value for the state(potentially in the same JS-cycle) then we need to make sure we don't trigger a new update before the old subscription chain is completed. [NL] + await Promise.resolve(); const ownerUnique = this.getOwnerContentTypeUnique(); if (!ownerUnique) return; const compositionUniques = contentTypeCompositions?.map((x) => x.contentType.unique) ?? []; @@ -359,7 +360,7 @@ export class UmbContentTypeStructureManager< this.#editedTypes.appendOne(toContentTypeUnique); // Find container. - const container = this.#containers.getValue().find((x) => x.id === containerId); + const container = (await firstValueFrom(this.#contentTypeContainers)).find((x) => x.id === containerId); if (!container) throw new Error('Container to clone was not found'); const clonedContainer: UmbPropertyTypeContainerModel = { @@ -433,38 +434,37 @@ export class UmbContentTypeStructureManager< sortOrder: sortOrder ?? 0, }; - // Ensure - this.ensureContainerNames(contentTypeUnique, type, parentId); - - const contentTypes = this.#contentTypes.getValue(); - const containers = [...(contentTypes.find((x) => x.unique === contentTypeUnique)?.containers ?? [])]; - containers.push(container); - - this.#contentTypes.updateOne(contentTypeUnique, { containers } as Partial); - - return container; + return this.insertContainer(contentTypeUnique, container); } - /*async insertContainer(contentTypeUnique: string | null, container: UmbPropertyTypeContainerModel) { + async insertContainer(contentTypeUnique: string | null, container: UmbPropertyTypeContainerModel) { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; + const newContainer = { ...container }; + const type = newContainer.type; + const parentId = newContainer.parent?.id ?? null; // If we have a parent, we need to ensure it exists, and then update the parent property with the new container id. - if (container.parent) { - const parentContainer = await this.ensureContainerOf(container.parent.id, contentTypeUnique); + if (newContainer.parent) { + const parentContainer = await this.ensureContainerOf(newContainer.parent.id, contentTypeUnique); if (!parentContainer) { throw new Error('Container for inserting property could not be found or created'); } - container.parent.id = parentContainer.id; + newContainer.parent.id = parentContainer.id; } + // Ensure + this.ensureContainerNames(contentTypeUnique, type, parentId); + const frozenContainers = this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique)?.containers ?? []; - const containers = appendToFrozenArray(frozenContainers, container, (x) => x.id === container.id); + const containers = appendToFrozenArray(frozenContainers, newContainer, (x) => x.id === newContainer.id); this.#contentTypes.updateOne(contentTypeUnique, { containers } as Partial); - }*/ + + return newContainer; + } makeEmptyContainerName( containerId: string, @@ -537,7 +537,11 @@ export class UmbContentTypeStructureManager< this.#contentTypes.updateOne(contentTypeUnique, { containers }); } - async removeContainer(contentTypeUnique: string | null, containerId: string | null = null) { + async removeContainer( + contentTypeUnique: string | null, + containerId: string | null = null, + args?: { preventRemovingProperties?: boolean }, + ): Promise { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; this.#editedTypes.appendOne(contentTypeUnique); @@ -552,12 +556,15 @@ export class UmbContentTypeStructureManager< .map((x) => x.id); const containers = frozenContainers.filter((x) => x.id !== containerId && x.parent?.id !== containerId); - const frozenProperties = contentType.properties; - const properties = frozenProperties.filter((x) => - x.container ? !removedContainerIds.some((ids) => ids === x.container?.id) : true, - ); + const updates: Partial = { containers } as Partial; - this.#contentTypes.updateOne(contentTypeUnique, { containers, properties } as Partial); + if (args?.preventRemovingProperties !== true) { + updates.properties = contentType.properties.filter((x) => + x.container ? !removedContainerIds.some((ids) => ids === x.container?.id) : true, + ); + } + + this.#contentTypes.updateOne(contentTypeUnique, updates); } async insertProperty(contentTypeUnique: string | null, property: UmbPropertyTypeModel) { @@ -655,6 +662,11 @@ export class UmbContentTypeStructureManager< return undefined; } + async getOwnerPropertyById(propertyUnique: string | null): Promise { + await this.#init; + return this.getOwnerContentType()?.properties?.find((property) => property.unique === propertyUnique); + } + async getPropertyStructureByAlias(propertyAlias: string) { await this.#init; for (const docType of this.#contentTypes.getValue()) { @@ -695,17 +707,19 @@ export class UmbContentTypeStructureManager< } rootContainers(containerType: UmbPropertyContainerTypes) { - return this.#containers.asObservablePart((data) => { + return createObservablePart(this.#contentTypeContainers, (data) => { return data.filter((x) => x.parent === null && x.type === containerType); }); } - getRootContainers(containerType: UmbPropertyContainerTypes) { - return this.#containers.getValue().filter((x) => x.parent === null && x.type === containerType); + async getRootContainers(containerType: UmbPropertyContainerTypes) { + return (await firstValueFrom(this.#contentTypeContainers)).filter( + (x) => x.parent === null && x.type === containerType, + ); } async hasRootContainers(containerType: UmbPropertyContainerTypes) { - return this.#containers.asObservablePart((data) => { + return createObservablePart(this.#contentTypeContainers, (data) => { return data.filter((x) => x.parent === null && x.type === containerType).length > 0; }); } @@ -719,7 +733,14 @@ export class UmbContentTypeStructureManager< ); } - getOwnerContainers(containerType: UmbPropertyContainerTypes, parentId: string | null) { + getOwnerContainerById(id: string | null): UmbPropertyTypeContainerModel | undefined { + return this.getOwnerContentType()?.containers?.find((x) => x.id === id); + } + + getOwnerContainers( + containerType: UmbPropertyContainerTypes, + parentId: string | null, + ): Array | undefined { return this.getOwnerContentType()?.containers?.filter( (x) => (parentId ? x.parent?.id === parentId : x.parent === null) && x.type === containerType, ); @@ -730,14 +751,14 @@ export class UmbContentTypeStructureManager< } containersOfParentId(parentId: string, containerType: UmbPropertyContainerTypes) { - return this.#containers.asObservablePart((data) => { + return createObservablePart(this.#contentTypeContainers, (data) => { return data.filter((x) => x.parent?.id === parentId && x.type === containerType); }); } // In future this might need to take parentName(parentId lookup) into account as well? otherwise containers that share same name and type will always be merged, but their position might be different and they should not be merged. [NL] containersByNameAndType(name: string, containerType: UmbPropertyContainerTypes) { - return this.#containers.asObservablePart((data) => { + return createObservablePart(this.#contentTypeContainers, (data) => { return data.filter((x) => x.name === name && x.type === containerType); }); } @@ -748,7 +769,7 @@ export class UmbContentTypeStructureManager< parentName: string | null, parentType?: UmbPropertyContainerTypes, ) { - return this.#containers.asObservablePart((data) => { + return createObservablePart(this.#contentTypeContainers, (data) => { return data.filter( (x) => // Match name and type: @@ -795,17 +816,27 @@ export class UmbContentTypeStructureManager< ); } + /** + * Get all property aliases for the content type including inherited and composed content types. + * @returns {Promise>} - A promise that will be resolved with the list of all content type property aliases. + */ + async getContentTypePropertyAliases() { + return this.#contentTypes + .getValue() + .flatMap((x) => x.properties?.map((y) => y.alias) ?? []) + .filter(UmbFilterDuplicateStrings); + } + #clear() { this.#contentTypeObservers.forEach((observer) => observer.destroy()); this.#contentTypeObservers = []; - this.#containers.setValue([]); this.#repoManager?.clear(); this.#contentTypes.setValue([]); + this.#ownerContentTypeUnique = undefined; } public override destroy() { this.#contentTypes.destroy(); - this.#containers.destroy(); super.destroy(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts index a2b0cd669f..068eaed33c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts @@ -82,7 +82,7 @@ export abstract class UmbContentTypeWorkspaceContextBase< ): Promise { this.resetState(); this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Creating ${this.getEntityType()} scaffold` }); - this.setParent(args.parent); + this._internal_setCreateUnderParent(args.parent); const request = this.structure.createScaffold(args.preset); this._getDataPromise = request; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts index 0f92a8e7a6..cbfedf1878 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts @@ -97,6 +97,35 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement { i++; } }, + onRequestDrop: async ({ unique }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + return context.structure.getOwnerPropertyById(unique); + }, + requestExternalRemove: async ({ item }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + return await context.structure.removeProperty(null, item.unique).then( + () => true, + () => false, + ); + }, + requestExternalInsert: async ({ item }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + const parent = this._containerId ? { id: this._containerId } : null; + const updatedItem = { ...item, parent }; + return await context.structure.insertProperty(null, updatedItem).then( + () => true, + () => false, + ); + }, }); private _containerId: string | null | undefined; @@ -152,7 +181,7 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement { constructor() { super(); - this.#sorter.disable(); + //this.#sorter.disable(); this.consumeContext(UMB_CONTENT_TYPE_DESIGN_EDITOR_CONTEXT, (context) => { this.observe( @@ -160,9 +189,9 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement { (isSorting) => { this._sortModeActive = isSorting; if (isSorting) { - this.#sorter.enable(); + //this.#sorter.enable(); } else { - this.#sorter.disable(); + //this.#sorter.disable(); } }, '_observeIsSorting', @@ -305,6 +334,16 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement { static override styles = [ UmbTextStyles, css` + :host { + display: block; + } + + #property-list { + /* enables dropping things into this despite it begin empty. */ + margin-top: -20px; + padding-top: 20px; + } + #btn-add { width: 100%; --uui-button-height: var(--uui-size-14); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts index 7494c47cf6..78a74aa86f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-tab.element.ts @@ -72,6 +72,35 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement { i++; } }, + onRequestDrop: async ({ unique }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + return context.structure.getOwnerContainerById(unique); + }, + requestExternalRemove: async ({ item }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + return await context.structure.removeContainer(null, item.id, { preventRemovingProperties: true }).then( + () => true, + () => false, + ); + }, + requestExternalInsert: async ({ item }) => { + const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT); + if (!context) { + throw new Error('Could not get Workspace Context'); + } + const parent = this.#containerId ? { id: this.#containerId } : null; + const updatedItem = { ...item, parent }; + return await context.structure.insertContainer(null, updatedItem).then( + () => true, + () => false, + ); + }, }); #workspaceModal?: UmbModalRouteRegistrationController< @@ -231,9 +260,10 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement { .container-list { display: grid; gap: 10px; + align-content: start; } - #convert-to-tab { + .container-list #convert-to-tab { margin-bottom: var(--uui-size-layout-1); display: flex; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts index 909af69582..34d0e5d3a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -389,17 +389,25 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements const currentOwnerCompositionCompositions = currentOwnerCompositions.filter( (composition) => composition.compositionType === CompositionTypeModel.COMPOSITION, ); + + const currentOwnerCompositionCompositionUniques = currentOwnerCompositionCompositions.map( + (composition) => composition.contentType.unique, + ); + const currentOwnerInheritanceCompositions = currentOwnerCompositions.filter( (composition) => composition.compositionType === CompositionTypeModel.INHERITANCE, ); + const currentPropertyAliases = await this.#workspaceContext.structure.getContentTypePropertyAliases(); + const compositionConfiguration = { compositionRepositoryAlias: this._compositionRepositoryAlias, unique: unique, - selection: currentOwnerCompositionCompositions.map((composition) => composition.contentType.unique), + selection: currentOwnerCompositionCompositionUniques, usedForInheritance: currentInheritanceCompositions.map((composition) => composition.contentType.unique), + usedForComposition: currentOwnerCompositionCompositionUniques, isElement: ownerContentType.isElement, - currentPropertyAliases: [], + currentPropertyAliases, isNew: this.#workspaceContext.getIsNew()!, }; @@ -423,6 +431,12 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements ]); } + #onDragOver(event: DragEvent, path: string) { + if (this._activePath === path) return; + event.preventDefault(); + window.history.replaceState(null, '', path); + } + override render() { return html` @@ -499,8 +513,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements } renderRootTab() { - const rootTabPath = this._routerPath + '/root'; - const rootTabActive = rootTabPath === this._activePath; + const path = this._routerPath + '/root'; + const rootTabActive = path === this._activePath; if (!this._hasRootGroups && !this._sortModeActive) { // If we don't have any root groups and we are not in sort mode, then we don't want to render the root tab. return nothing; @@ -512,7 +526,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements class=${this._hasRootGroups || rootTabActive ? '' : 'content-tab-is-empty'} label=${this.localize.term('general_generic')} .active=${rootTabActive} - href=${rootTabPath}> + href=${path} + @dragover=${(event: DragEvent) => this.#onDragOver(event, path)}> ${this.localize.term('general_generic')} `; @@ -529,7 +544,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements href=${path} data-umb-tab-id=${ifDefined(tab.id)} data-mark="tab:${tab.name}" - ?sortable=${ownedTab}> + ?sortable=${ownedTab} + @dragover=${(event: DragEvent) => this.#onDragOver(event, path)}> ${this.renderTabInner(tab, tabActive, ownedTab)} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/property-dataset-context/element-property-dataset.context.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/property-dataset-context/element-property-dataset.context.ts index 96381eb39f..3ba55f14b9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/property-dataset-context/element-property-dataset.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/property-dataset-context/element-property-dataset.context.ts @@ -12,7 +12,7 @@ import { createObservablePart, mergeObservables, } from '@umbraco-cms/backoffice/observable-api'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantContext, UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbContentTypeModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; @@ -47,6 +47,8 @@ export abstract class UmbElementPropertyDatasetContext< protected _readOnly = new UmbBooleanState(false); public readOnly = this._readOnly.asObservable(); + #variantContext = new UmbVariantContext(this).inherit(); + getEntityType(): string { return this._dataOwner.getEntityType(); } @@ -64,6 +66,7 @@ export abstract class UmbElementPropertyDatasetContext< super(host, UMB_PROPERTY_DATASET_CONTEXT); this._dataOwner = dataOwner; this.#variantId = variantId ?? UmbVariantId.CreateInvariant(); + this.#variantContext.setVariantId(this.#variantId); this.#propertyVariantIdPromise = new Promise((resolve) => { this.#propertyVariantIdPromiseResolver = resolve as any; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index 3c257fbb1c..1420b1e56e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -714,6 +714,10 @@ export abstract class UmbContentDetailWorkspaceContextBase< */ public async runMandatoryValidationForSaveData(saveData: DetailModelType, variantIds: Array = []) { // Check that the data is valid before we save it. + // If we vary by culture then we do not want to validate the invariant variant. + if (this.getVariesByCulture()) { + variantIds = variantIds.filter((variant) => !variant.isCultureInvariant()); + } const missingVariants = variantIds.filter((variant) => { return !saveData.variants.some((y) => variant.compare(y)); }); @@ -754,7 +758,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< // We ask the server first to get a concatenated set of validation messages. So we see both front-end and back-end validation messages [NL] if (this.getIsNew()) { - const parent = this.getParent(); + const parent = this._internal_getCreateUnderParent(); if (!parent) throw new Error('Parent is not set'); await this.#serverValidation.askServerForValidation( saveData, @@ -885,7 +889,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< async #create(variantIds: Array, saveData: DetailModelType) { if (!this._detailRepository) throw new Error('Detail repository is not set'); - const parent = this.getParent(); + const parent = this._internal_getCreateUnderParent(); if (!parent) throw new Error('Parent is not set'); const { data, error } = await this._detailRepository.create(saveData, parent.unique); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor-properties.element.ts index 0e5fc652a9..797722426c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor-properties.element.ts @@ -29,7 +29,7 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement } @state() - _variantId?: UmbVariantId; + _datasetVariantId?: UmbVariantId; @state() _visibleProperties?: Array; @@ -38,7 +38,7 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement super(); this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => { - this._variantId = datasetContext?.getVariantId(); + this._datasetVariantId = datasetContext?.getVariantId(); this.#processPropertyStructure(); }); @@ -61,16 +61,19 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement } #processPropertyStructure() { - if (!this.#workspaceContext || !this.#properties || !this.#propertyStructureHelper) { + if (!this.#workspaceContext || !this.#properties || !this.#propertyStructureHelper || !this._datasetVariantId) { return; } const propertyViewGuard = this.#workspaceContext.propertyViewGuard; this.#properties.forEach((property) => { - const propertyVariantId = new UmbVariantId(this._variantId?.culture, this._variantId?.segment); + const propertyVariantId = new UmbVariantId( + property.variesByCulture ? this._datasetVariantId?.culture : null, + property.variesBySegment ? this._datasetVariantId?.segment : null, + ); this.observe( - propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property), + propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property, this._datasetVariantId!), (permitted) => { if (permitted) { this.#visiblePropertiesUniques.push(property.unique); @@ -95,14 +98,14 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement } override render() { - return this._variantId && this._visibleProperties + return this._datasetVariantId && this._visibleProperties ? repeat( this._visibleProperties, (property) => property.alias, (property) => html``, ) : nothing; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor-property.element.ts index b7f3396900..eb502db256 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor-property.element.ts @@ -46,7 +46,11 @@ export class UmbContentWorkspaceViewEditPropertyElement extends UmbLitElement { })}].value`; this.observe( - this._context.propertyWriteGuard.isPermittedForVariantAndProperty(propertyVariantId, this.property), + this._context.propertyWriteGuard.isPermittedForVariantAndProperty( + propertyVariantId, + this.property, + this.variantId, + ), (write) => { this._writeable = write; }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts index 883677cc43..d442a7c153 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts @@ -6700,6 +6700,7 @@ export type GetItemDocumentSearchData = { query?: { query?: string; trashed?: boolean; + culture?: string; skip?: number; take?: number; parentId?: string; @@ -8991,6 +8992,7 @@ export type GetItemMediaSearchData = { query?: { query?: string; trashed?: boolean; + culture?: string; skip?: number; take?: number; parentId?: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 9967dead34..4edbcf886c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -25,7 +25,7 @@ import { } from '@umbraco-cms/backoffice/entity-action'; import type { UmbActionEventContext } from '@umbraco-cms/backoffice/action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; -import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; +import { UMB_ENTITY_CONTEXT, UmbParentEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController, type UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; @@ -83,6 +83,7 @@ export class UmbDefaultCollectionContext< }); #actionEventContext: UmbActionEventContext | undefined; + #parentEntityContext = new UmbParentEntityContext(this); constructor(host: UmbControllerHost, defaultViewAlias: string, defaultFilter: Partial = {}) { super(host, UMB_COLLECTION_CONTEXT); @@ -92,6 +93,23 @@ export class UmbDefaultCollectionContext< this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange); this.#listenToEntityEvents(); + + // The parent entity context is used to get the parent entity for the collection items + // All items in the collection are children of the current entity context + this.consumeContext(UMB_ENTITY_CONTEXT, (context) => { + const currentEntityUnique = context?.getUnique(); + const currentEntityType = context?.getEntityType(); + + const parent: UmbEntityModel | undefined = + currentEntityUnique && currentEntityType + ? { + unique: currentEntityUnique, + entityType: currentEntityType, + } + : undefined; + + this.#parentEntityContext?.setParent(parent); + }); } setupView(viewElement: UmbControllerHost) { @@ -225,6 +243,10 @@ export class UmbDefaultCollectionContext< return this._manifest; } + public getEmptyLabel(): string { + return this.manifest?.meta.noItemsLabel ?? this.#config?.noItemsLabel ?? '#collection_noItemsTitle'; + } + /** * Requests the collection from the repository. * @returns {*} @@ -261,6 +283,10 @@ export class UmbDefaultCollectionContext< this.requestCollection(); } + public updateFilter(filter: Partial) { + this._filter.setValue({ ...this._filter.getValue(), ...filter }); + } + public getLastSelectedView(unique: string | undefined): string | undefined { if (!unique) return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts index aa08150a8b..94440fa140 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts @@ -23,6 +23,9 @@ umbExtensionsRegistry.register(manifest); @customElement('umb-collection-default') export class UmbCollectionDefaultElement extends UmbLitElement { + // + #collectionContext?: UmbDefaultCollectionContext; + @state() private _routes: Array = []; @@ -32,7 +35,8 @@ export class UmbCollectionDefaultElement extends UmbLitElement { @state() private _isDoneLoading = false; - #collectionContext?: UmbDefaultCollectionContext; + @state() + private _emptyLabel?: string; constructor() { super(); @@ -40,6 +44,7 @@ export class UmbCollectionDefaultElement extends UmbLitElement { this.#collectionContext = context; this.#observeCollectionRoutes(); this.#observeTotalItems(); + this.#getEmptyStateLabel(); await this.#collectionContext?.requestCollection(); this._isDoneLoading = true; }); @@ -69,6 +74,10 @@ export class UmbCollectionDefaultElement extends UmbLitElement { ); } + #getEmptyStateLabel() { + this._emptyLabel = this.#collectionContext?.getEmptyLabel(); + } + override render() { return this._routes ? html` @@ -98,9 +107,10 @@ export class UmbCollectionDefaultElement extends UmbLitElement { #renderEmptyState() { if (!this._isDoneLoading) return nothing; + return html`
-

+

${this.localize.string(this._emptyLabel)}

`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/collection.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/collection.extension.ts index 7532fe9acc..56011a2cb8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/collection.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/collection.extension.ts @@ -9,6 +9,7 @@ export interface ManifestCollection export interface MetaCollection { repositoryAlias: string; + noItemsLabel?: string; } declare global { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts index f449868688..3314fef940 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts @@ -25,6 +25,7 @@ export interface UmbCollectionConfiguration { orderBy?: string; orderDirection?: string; pageSize?: number; + noItemsLabel?: string; userDefinedProperties?: Array; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/dropdown/dropdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/dropdown/dropdown.element.ts index a9a064d6d3..cc8a844b48 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/dropdown/dropdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/dropdown/dropdown.element.ts @@ -12,13 +12,11 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; // TODO: maybe move this to UI Library. @customElement('umb-dropdown') export class UmbDropdownElement extends UmbLitElement { - @query('#dropdown-popover') - popoverContainerElement?: UUIPopoverContainerElement; @property({ type: Boolean, reflect: true }) open = false; @property() - label = ''; + label?: string; @property() look: UUIInterfaceLook = 'default'; @@ -35,19 +33,16 @@ export class UmbDropdownElement extends UmbLitElement { @property({ type: Boolean, attribute: 'hide-expand' }) hideExpand = false; + @query('#dropdown-popover') + popoverContainerElement?: UUIPopoverContainerElement; + protected override updated(_changedProperties: PropertyValueMap | Map): void { super.updated(_changedProperties); if (_changedProperties.has('open') && this.popoverContainerElement) { if (this.open) { - // 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.popoverContainerElement.showPopover(); + this.openDropdown(); } else { - // 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.popoverContainerElement.hidePopover(); + this.closeDropdown(); } } } @@ -59,14 +54,29 @@ export class UmbDropdownElement extends UmbLitElement { this.open = event.newState === 'open'; } + openDropdown() { + // 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.popoverContainerElement?.showPopover(); + } + + closeDropdown() { + // 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.popoverContainerElement?.hidePopover(); + } + override render() { return html` ${when( diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts index 1467fee2b9..ec97aa8cf3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts @@ -1,7 +1,17 @@ import { UmbEntityContext } from '../../entity/entity.context.js'; +import type { UmbDropdownElement } from '../dropdown/index.js'; import type { UmbEntityAction, ManifestEntityActionDefaultKind } from '@umbraco-cms/backoffice/entity-action'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; -import { html, nothing, customElement, property, state, ifDefined, css } from '@umbraco-cms/backoffice/external/lit'; +import { + html, + nothing, + customElement, + property, + state, + ifDefined, + css, + query, +} from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; @@ -29,8 +39,8 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { @state() private _firstActionHref?: string; - @state() - _dropdownIsOpen = false; + @query('#action-modal') + private _dropdownElement?: UmbDropdownElement; // TODO: provide the entity context on a higher level, like the root element of this entity, tree-item/workspace/... [NL] #entityContext = new UmbEntityContext(this); @@ -79,7 +89,7 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { } #onActionExecuted() { - this._dropdownIsOpen = false; + this._dropdownElement?.closeDropdown(); } #onDropdownClick(event: Event) { @@ -95,25 +105,27 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { if (this._numberOfActions === 1) return nothing; return html` - - + + + .unique=${this.unique} + .label=${this.label}> `; } #renderFirstAction() { - if (!this._firstActionApi) return nothing; + if (!this._firstActionApi || !this._firstActionManifest) return nothing; return html` - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/default/entity-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/default/entity-action.element.ts index daccf37e70..c3ac0d406f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/default/entity-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/default/entity-action.element.ts @@ -66,17 +66,18 @@ export class UmbEntityActionDefaultElement< } override render() { - const label = this.manifest?.meta.label ? this.localize.string(this.manifest.meta.label) : this.manifest?.name; + if (!this.manifest) return nothing; + + const label = this.manifest.meta.label ? this.localize.string(this.manifest.meta.label) : this.manifest.name; return html` - ${this.manifest?.meta.icon - ? html`` - : nothing} + ${this.manifest.meta.icon ? html`` : nothing} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts index f1b112ff09..d2ca007786 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts @@ -1,17 +1,20 @@ -import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbEntityModel, UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity'; import { html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-entity-actions-table-column-view') export class UmbEntityActionsTableColumnViewElement extends UmbLitElement { @property({ attribute: false }) - value?: UmbEntityModel; + value?: UmbEntityModel | UmbNamedEntityModel; override render() { if (!this.value) return nothing; return html` - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts index f239f3ebe4..275fdf48fe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts @@ -8,6 +8,7 @@ import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import './default-item-ref.element.js'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; @customElement('umb-entity-item-ref') export class UmbEntityItemRefElement extends UmbLitElement { @@ -41,7 +42,7 @@ export class UmbEntityItemRefElement extends UmbLitElement { } #readonly = false; - @property({ type: Boolean, attribute: 'readonly' }) + @property({ type: Boolean, reflect: true }) public get readonly() { return this.#readonly; } @@ -54,7 +55,7 @@ export class UmbEntityItemRefElement extends UmbLitElement { } #standalone = false; - @property({ type: Boolean, attribute: 'standalone' }) + @property({ type: Boolean, reflect: true }) public get standalone() { return this.#standalone; } @@ -66,8 +67,74 @@ export class UmbEntityItemRefElement extends UmbLitElement { } } + #selectOnly = false; + @property({ type: Boolean, attribute: 'select-only', reflect: true }) + public get selectOnly() { + return this.#selectOnly; + } + public set selectOnly(value) { + this.#selectOnly = value; + + if (this._component) { + this._component.selectOnly = this.#selectOnly; + } + } + + #selectable = false; + @property({ type: Boolean, reflect: true }) + public get selectable() { + return this.#selectable; + } + public set selectable(value) { + this.#selectable = value; + + if (this._component) { + this._component.selectable = this.#selectable; + } + } + + #selected = false; + @property({ type: Boolean, reflect: true }) + public get selected() { + return this.#selected; + } + public set selected(value) { + this.#selected = value; + + if (this._component) { + this._component.selected = this.#selected; + } + } + + #disabled = false; + @property({ type: Boolean, reflect: true }) + public get disabled() { + return this.#disabled; + } + public set disabled(value) { + this.#disabled = value; + + if (this._component) { + this._component.disabled = this.#disabled; + } + } + #pathAddendum = new UmbRoutePathAddendumContext(this); + #onSelected(event: UmbSelectedEvent) { + event.stopPropagation(); + const unique = this.#item?.unique; + if (!unique) throw new Error('No unique id found for item'); + this.dispatchEvent(new UmbSelectedEvent(unique)); + } + + #onDeselected(event: UmbDeselectedEvent) { + event.stopPropagation(); + const unique = this.#item?.unique; + if (!unique) throw new Error('No unique id found for item'); + this.dispatchEvent(new UmbDeselectedEvent(unique)); + } + protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void { super.firstUpdated(_changedProperties); this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, 'entity-item-ref'); @@ -91,6 +158,13 @@ export class UmbEntityItemRefElement extends UmbLitElement { component.item = this.#item; component.readonly = this.readonly; component.standalone = this.standalone; + component.selectOnly = this.selectOnly; + component.selectable = this.selectable; + component.selected = this.selected; + component.disabled = this.disabled; + + component.addEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); + component.addEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); // Proxy the actions slot to the component const slotElement = document.createElement('slot'); @@ -110,6 +184,12 @@ export class UmbEntityItemRefElement extends UmbLitElement { return html`${this._component}`; } + override destroy(): void { + this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); + this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); + super.destroy(); + } + static override styles = [ css` :host { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts index 5ada9eda75..a0ee69f820 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts @@ -1 +1,2 @@ +export * from './item-data-api-get-request-controller/index.js'; export * from './entity-item-ref/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/index.ts new file mode 100644 index 0000000000..5bd7a5f06f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/index.ts @@ -0,0 +1 @@ +export * from './item-data-api-get-request.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/item-data-api-get-request.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/item-data-api-get-request.controller.ts new file mode 100644 index 0000000000..b10a783b97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/item-data-api-get-request.controller.ts @@ -0,0 +1,66 @@ +import type { UmbItemDataApiGetRequestControllerArgs } from './types.js'; +import { + batchTryExecute, + tryExecute, + UmbError, + type UmbApiError, + type UmbCancelError, + type UmbDataApiResponse, +} from '@umbraco-cms/backoffice/resources'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { batchArray } from '@umbraco-cms/backoffice/utils'; +import { umbPeekError } from '@umbraco-cms/backoffice/notification'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +export class UmbItemDataApiGetRequestController< + ResponseModelType extends UmbDataApiResponse, +> extends UmbControllerBase { + #apiCallback: (args: { uniques: Array }) => Promise; + #uniques: Array; + #batchSize: number = 40; + + constructor(host: UmbControllerHost, args: UmbItemDataApiGetRequestControllerArgs) { + super(host); + this.#apiCallback = args.api; + this.#uniques = args.uniques; + } + + async request() { + if (!this.#uniques) throw new Error('Uniques are missing'); + + let data: ResponseModelType['data'] | undefined; + let error: UmbError | UmbApiError | UmbCancelError | Error | undefined; + + if (this.#uniques.length > this.#batchSize) { + const chunks = batchArray(this.#uniques, this.#batchSize); + const results = await batchTryExecute(this, chunks, (chunk) => this.#apiCallback({ uniques: chunk })); + + const errors = results.filter((promiseResult) => promiseResult.status === 'rejected'); + + if (errors.length > 0) { + error = await this.#getAndHandleErrorResult(errors); + } + + data = results + .filter((promiseResult) => promiseResult.status === 'fulfilled') + .flatMap((promiseResult) => promiseResult.value.data); + } else { + const result = await tryExecute(this, this.#apiCallback({ uniques: this.#uniques })); + data = result.data; + error = result.error; + } + + return { data, error }; + } + + async #getAndHandleErrorResult(errors: Array) { + // TODO: We currently expect all the errors to be the same, but we should handle this better in the future. + const error = errors[0]; + await umbPeekError(this, { + headline: 'Error fetching items', + message: 'An error occurred while fetching items.', + }); + + return new UmbError(error.reason); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/types.ts new file mode 100644 index 0000000000..fc4f1a47e4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/item-data-api-get-request-controller/types.ts @@ -0,0 +1,6 @@ +import type { UmbDataApiResponse } from '@umbraco-cms/backoffice/resources'; + +export interface UmbItemDataApiGetRequestControllerArgs { + api: (args: { uniques: Array }) => Promise; + uniques: Array; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts index 31240f7f67..78aae14245 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts @@ -1,4 +1,5 @@ import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity'; +export type * from './item-data-api-get-request-controller/types.js'; export interface UmbDefaultItemModel extends UmbNamedEntityModel { icon?: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/constants.ts index 3ed4ee95ed..f804358649 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/constants.ts @@ -1 +1,2 @@ export * from './contexts/ancestors/constants.js'; +export * from './contexts/parent/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/constants.ts new file mode 100644 index 0000000000..31caa05f93 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/constants.ts @@ -0,0 +1 @@ +export { UMB_PARENT_ENTITY_CONTEXT } from './parent.entity-context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/index.ts new file mode 100644 index 0000000000..d499a54ed5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/index.ts @@ -0,0 +1 @@ +export { UmbParentEntityContext } from './parent.entity-context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/parent.entity-context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/parent.entity-context-token.ts new file mode 100644 index 0000000000..8a28553f5a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/parent.entity-context-token.ts @@ -0,0 +1,4 @@ +import type { UmbParentEntityContext } from './parent.entity-context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_PARENT_ENTITY_CONTEXT = new UmbContextToken('UmbParentEntityContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/parent.entity-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/parent.entity-context.ts new file mode 100644 index 0000000000..efc1fed494 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/contexts/parent/parent.entity-context.ts @@ -0,0 +1,38 @@ +import type { UmbEntityModel } from '../../types.js'; +import { UMB_PARENT_ENTITY_CONTEXT } from './parent.entity-context-token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; + +/** + * A entity context for the parent + * @class UmbParentEntityContext + * @augments {UmbContextBase} + * @implements {UmbParentEntityContext} + */ +export class UmbParentEntityContext extends UmbContextBase { + #parent = new UmbObjectState(undefined); + parent = this.#parent.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_PARENT_ENTITY_CONTEXT); + } + + /** + * Gets the parent state + * @returns {UmbEntityModel | undefined} - The parent state + * @memberof UmbParentEntityContext + */ + getParent(): UmbEntityModel | undefined { + return this.#parent.getValue(); + } + + /** + * Sets the parent state + * @param {UmbEntityModel | undefined} parent - The parent state + * @memberof UmbParentEntityContext + */ + setParent(parent: UmbEntityModel | undefined): void { + this.#parent.setValue(parent); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts index d064b7c97a..046856fa0e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts @@ -2,4 +2,6 @@ export { UMB_ENTITY_CONTEXT } from './entity.context-token.js'; export { UmbEntityContext } from './entity.context.js'; export * from './constants.js'; export * from './contexts/ancestors/index.js'; +export * from './contexts/parent/index.js'; + export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/components/menu-item-layout/menu-item-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/components/menu-item-layout/menu-item-layout.element.ts index b6471e064d..fdc2c07b7a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/components/menu-item-layout/menu-item-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/components/menu-item-layout/menu-item-layout.element.ts @@ -1,5 +1,6 @@ import { html, customElement, property, ifDefined, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { ensureSlash } from '@umbraco-cms/backoffice/router'; import { debounce } from '@umbraco-cms/backoffice/utils'; /** @@ -62,8 +63,12 @@ export class UmbMenuItemLayoutElement extends UmbLitElement { return; } - const location = window.location.pathname; - this._isActive = location.includes(this.href); + /* Check if the current location includes the href of this menu item + We ensure that the paths ends with a slash to avoid collisions with paths like /path-1 and /path-1-2 where /path is in both. + Instead we compare /path-1/ with /path-1-2/ which wont collide.*/ + const location = ensureSlash(window.location.pathname); + const compareHref = ensureSlash(this.href); + this._isActive = location.includes(compareHref); } override render() { @@ -80,7 +85,7 @@ export class UmbMenuItemLayoutElement extends UmbLitElement { slot="actions" .entityType=${this.entityType} .unique=${null} - .label=${this.label}> + .label=${this.localize.term('actions_viewActionsFor', [this.label])}> ` : ''} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/index.ts index 46a6c42b4b..c7d6c36f9e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/index.ts @@ -1,5 +1,7 @@ export * from './components/index.js'; export * from './menu-tree-structure-workspace-context-base.js'; +export * from './menu-structure-workspace-context.context-token.js'; +export * from './menu-variant-structure-workspace-context.context-token.js'; export * from './menu-variant-tree-structure-workspace-context-base.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-structure-workspace-context.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-structure-workspace-context.context-token.ts new file mode 100644 index 0000000000..e04fca4af2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-structure-workspace-context.context-token.ts @@ -0,0 +1,7 @@ +import type { UmbMenuStructureWorkspaceContext } from './menu-structure-workspace-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', + 'UmbMenuStructure', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts index d461bff391..5ebd0dc2ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts @@ -1,32 +1,41 @@ import type { UmbStructureItemModel } from './types.js'; +import { UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT } from './menu-structure-workspace-context.context-token.js'; import type { UmbTreeRepository, UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; -import { UMB_SUBMITTABLE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbAncestorsEntityContext, UmbParentEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity'; interface UmbMenuTreeStructureWorkspaceContextBaseArgs { treeRepositoryAlias: string; } +// TODO: introduce base class for all menu structure workspaces to handle ancestors and parent export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContextBase { - #workspaceContext?: typeof UMB_SUBMITTABLE_WORKSPACE_CONTEXT.TYPE; + #workspaceContext?: typeof UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT.TYPE; #args: UmbMenuTreeStructureWorkspaceContextBaseArgs; #structure = new UmbArrayState([], (x) => x.unique); public readonly structure = this.#structure.asObservable(); #parent = new UmbObjectState(undefined); + /** + * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead. + */ public readonly parent = this.#parent.asObservable(); + #parentContext = new UmbParentEntityContext(this); + #ancestorContext = new UmbAncestorsEntityContext(this); + constructor(host: UmbControllerHost, args: UmbMenuTreeStructureWorkspaceContextBaseArgs) { - // TODO: set up context token - super(host, 'UmbMenuStructureWorkspaceContext'); + super(host, UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT); + // 'UmbMenuStructureWorkspaceContext' is Obsolete, will be removed in v.18 + this.provideContext('UmbMenuStructureWorkspaceContext', this); this.#args = args; - // TODO: set up context token that supports parentEntityType, parentUnique, entityType. - this.consumeContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT, (instance) => { + this.consumeContext(UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT, (instance) => { this.#workspaceContext = instance; this.observe(this.#workspaceContext?.unique, (value) => { if (!value) return; @@ -59,14 +68,16 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex const isNew = this.#workspaceContext?.getIsNew(); const entityTypeObservable = isNew - ? (this.#workspaceContext as any)?.parentEntityType - : (this.#workspaceContext as any).entityType; + ? this.#workspaceContext?._internal_createUnderParentEntityType + : this.#workspaceContext?.entityType; const entityType = (await this.observe(entityTypeObservable, () => {})?.asPromise()) as string; if (!entityType) throw new Error('Entity type is not available'); // If the entity type is different from the root entity type, then we can request the ancestors. if (entityType !== root?.entityType) { - const uniqueObservable = isNew ? (this.#workspaceContext as any)?.parentUnique : this.#workspaceContext?.unique; + const uniqueObservable = isNew + ? this.#workspaceContext?._internal_createUnderParentEntityUnique + : this.#workspaceContext?.unique; const unique = (await this.observe(uniqueObservable, () => {})?.asPromise()) as string; if (!unique) throw new Error('Unique is not available'); @@ -83,11 +94,49 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex }); structureItems.push(...ancestorItems); + + this.#structure.setValue(structureItems); + this.#setParentData(structureItems); + this.#setAncestorData(data); } } + } - const parent = structureItems[structureItems.length - 2]; + #setParentData(structureItems: Array) { + /* If the item is not new, the current item is the last item in the array. + We filter out the current item unique to handle any case where it could show up */ + const parent = structureItems.filter((item) => item.unique !== this.#workspaceContext?.getUnique()).pop(); + + // TODO: remove this when the parent gets removed from the structure interface this.#parent.setValue(parent); - this.#structure.setValue(structureItems); + + const parentEntity = parent + ? { + unique: parent.unique, + entityType: parent.entityType, + } + : undefined; + + this.#parentContext.setParent(parentEntity); + } + + /* Notice: ancestors are based on the server "data" ancestors and are not based on the full Menu (UI) structure. + This will mean that any item placed in the data root will not have any ancestors. But will have a parent based on the UI structure. + */ + #setAncestorData(ancestors: Array) { + const ancestorEntities = ancestors + .map((treeItem) => { + const entity: UmbEntityModel = { + unique: treeItem.unique, + entityType: treeItem.entityType, + }; + + return entity; + }) + /* If the item is not new, the current item is the last item in the array. + We filter out the current item unique to handle any case where it could show up */ + .filter((item) => item.unique !== this.#workspaceContext?.getUnique()); + + this.#ancestorContext.setAncestors(ancestorEntities); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.context-token.ts new file mode 100644 index 0000000000..8cd8e1c750 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.context-token.ts @@ -0,0 +1,10 @@ +import type { UmbMenuVariantStructureWorkspaceContext } from './menu-variant-structure-workspace-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT = + new UmbContextToken( + 'UmbWorkspaceContext', + 'UmbMenuStructure', + (context): context is UmbMenuVariantStructureWorkspaceContext => + 'IS_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT' in context, + ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.interface.ts new file mode 100644 index 0000000000..ade079e3cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.interface.ts @@ -0,0 +1,7 @@ +import type { UmbVariantStructureItemModel } from './types.js'; +import type { UmbContext } from '@umbraco-cms/backoffice/class-api'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; + +export interface UmbMenuVariantStructureWorkspaceContext extends UmbContext { + structure: Observable; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts index 8268a9accc..1e483db90d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts @@ -1,36 +1,44 @@ import type { UmbVariantStructureItemModel } from './types.js'; -import type { UmbTreeRepository, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import { UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT } from './menu-variant-structure-workspace-context.context-token.js'; +import type { UmbTreeItemModel, UmbTreeRepository, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; -import { UMB_VARIANT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbAncestorsEntityContext } from '@umbraco-cms/backoffice/entity'; +import { UmbAncestorsEntityContext, UmbParentEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; interface UmbMenuVariantTreeStructureWorkspaceContextBaseArgs { treeRepositoryAlias: string; } +// TODO: introduce base class for all menu structure workspaces to handle ancestors and parent export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends UmbContextBase { - // TODO: add correct interface - #workspaceContext?: typeof UMB_VARIANT_WORKSPACE_CONTEXT.TYPE; + // + #workspaceContext?: typeof UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT.TYPE; #args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs; #structure = new UmbArrayState([], (x) => x.unique); public readonly structure = this.#structure.asObservable(); #parent = new UmbObjectState(undefined); + /** + * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead. + */ public readonly parent = this.#parent.asObservable(); + #parentContext = new UmbParentEntityContext(this); #ancestorContext = new UmbAncestorsEntityContext(this); + public readonly IS_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT = true; + constructor(host: UmbControllerHost, args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs) { - // TODO: set up context token - super(host, 'UmbMenuStructureWorkspaceContext'); + super(host, UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT); + // 'UmbMenuStructureWorkspaceContext' is Obsolete, will be removed in v.18 + this.provideContext('UmbMenuStructureWorkspaceContext', this); this.#args = args; - // TODO: Implement a Context Token that supports parentUnique, parentEntityType, entityType - this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (instance) => { + this.consumeContext(UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT, (instance) => { this.#workspaceContext = instance; this.observe( this.#workspaceContext?.unique, @@ -45,10 +53,12 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um async #requestStructure() { const isNew = this.#workspaceContext?.getIsNew(); - const uniqueObservable = isNew ? (this.#workspaceContext as any)?.parentUnique : this.#workspaceContext?.unique; + const uniqueObservable = isNew + ? this.#workspaceContext?._internal_createUnderParentEntityType + : this.#workspaceContext?.unique; const entityTypeObservable = isNew - ? (this.#workspaceContext as any)?.parentEntityType - : (this.#workspaceContext as any)?.entityType; + ? this.#workspaceContext?._internal_createUnderParentEntityUnique + : this.#workspaceContext?.entityType; let structureItems: Array = []; @@ -58,7 +68,7 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um const entityType = (await this.observe(entityTypeObservable, () => {})?.asPromise()) as string; if (!entityType) throw new Error('Entity type is not available'); - // TODO: add correct tree variant item model + // TODO: introduce variant tree item model const treeRepository = await createExtensionApiByAlias>( this, this.#args.treeRepositoryAlias, @@ -79,7 +89,7 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um const { data } = await treeRepository.requestTreeItemAncestors({ treeItem: { unique, entityType } }); if (data) { - const ancestorItems = data.map((treeItem) => { + const treeItemAncestors = data.map((treeItem) => { return { unique: treeItem.unique, entityType: treeItem.entityType, @@ -93,20 +103,49 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um }; }); - const ancestorEntities = data.map((treeItem) => { - return { + structureItems.push(...treeItemAncestors); + + this.#structure.setValue(structureItems); + this.#setParentData(structureItems); + this.#setAncestorData(data); + } + } + + #setParentData(structureItems: Array) { + /* If the item is not new, the current item is the last item in the array. + We filter out the current item unique to handle any case where it could show up */ + const parent = structureItems.filter((item) => item.unique !== this.#workspaceContext?.getUnique()).pop(); + + // TODO: remove this when the parent gets removed from the structure interface + this.#parent.setValue(parent); + + const parentEntity = parent + ? { + unique: parent.unique, + entityType: parent.entityType, + } + : undefined; + + this.#parentContext.setParent(parentEntity); + } + + /* Notice: ancestors are based on the server "data" ancestors and are not based on the full Menu (UI) structure. + This will mean that any item placed in the data root will not have any ancestors. But will have a parent based on the UI structure. + */ + #setAncestorData(ancestors: Array) { + const ancestorEntities = ancestors + .map((treeItem) => { + const entity: UmbEntityModel = { unique: treeItem.unique, entityType: treeItem.entityType, }; - }); - this.#ancestorContext.setAncestors(ancestorEntities); + return entity; + }) + /* If the item is not new, the current item is the last item in the array. + We filter out the current item unique to handle any case where it could show up */ + .filter((item) => item.unique !== this.#workspaceContext?.getUnique()); - structureItems.push(...ancestorItems); - - const parent = structureItems[structureItems.length - 2]; - this.#parent.setValue(parent); - this.#structure.setValue(structureItems); - } + this.#ancestorContext.setAncestors(ancestorEntities); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu-with-entity-actions/section-sidebar-menu-with-entity-actions.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu-with-entity-actions/section-sidebar-menu-with-entity-actions.element.ts index d54c73fa52..84d46fe2a3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu-with-entity-actions/section-sidebar-menu-with-entity-actions.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu-with-entity-actions/section-sidebar-menu-with-entity-actions.element.ts @@ -1,8 +1,9 @@ import { UmbSectionSidebarMenuElement } from '../section-sidebar-menu/section-sidebar-menu.element.js'; import type { ManifestSectionSidebarAppMenuWithEntityActionsKind } from '../section-sidebar-menu/types.js'; -import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, type PropertyValues, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbParentEntityContext } from '@umbraco-cms/backoffice/entity'; const manifestWithEntityActions: UmbExtensionManifestKind = { type: 'kind', @@ -18,15 +19,30 @@ umbExtensionsRegistry.register(manifestWithEntityActions); @customElement('umb-section-sidebar-menu-with-entity-actions') export class UmbSectionSidebarMenuWithEntityActionsElement extends UmbSectionSidebarMenuElement { + @state() + _unique = null; + + @state() + _entityType?: string | null; + + #parentContext = new UmbParentEntityContext(this); + + protected override updated(_changedProperties: PropertyValues): void { + if (_changedProperties.has('manifest')) { + const entityType = this.manifest?.meta.entityType; + this.#parentContext.setParent(entityType ? { unique: this._unique, entityType } : undefined); + } + } + override renderHeader() { return html` `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-result.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-result.element.ts index 4437d9b0b9..c6d41829c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-result.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-result.element.ts @@ -68,7 +68,6 @@ export class UmbPickerSearchResultElement extends UmbLitElement { } #renderResultItem(item: UmbEntityModel) { - console.log('pickableFilter', this.pickableFilter(item)); return html` + diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts index 9fbdd695bf..446496c0cf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts @@ -5,7 +5,7 @@ import type { UmbNameablePropertyDatasetContext } from './nameable-property-data import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbArrayState, UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantContext, UmbVariantId } from '@umbraco-cms/backoffice/variant'; /** * A base property dataset context implementation. @@ -32,26 +32,34 @@ export class UmbPropertyDatasetContextBase #readOnly = new UmbBooleanState(false); public readOnly = this.#readOnly.asObservable(); + #variantId: UmbVariantId = UmbVariantId.CreateInvariant(); + #variantContext = new UmbVariantContext(this).inherit(); + getEntityType() { return this._entityType; } + getUnique() { return this._unique; } + getName() { return this.#name.getValue(); } + setName(name: string | undefined) { this.#name.setValue(name); } + getVariantId() { - return UmbVariantId.CreateInvariant(); + return this.#variantId; } // variant id for a specific property? constructor(host: UmbControllerHost) { // The controller alias, is a very generic name cause we want only one of these for this controller host. super(host, UMB_PROPERTY_DATASET_CONTEXT); + this.#variantContext.setVariantId(this.getVariantId()); } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.test.ts index e4a95e3ca2..a0f04cc985 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.test.ts @@ -70,7 +70,7 @@ describe('UmbVariantPropertyGuardManager', () => { it('is not permitted for a variant when no states', (done) => { manager - .isPermittedForVariantAndProperty(invariantVariant, propB) + .isPermittedForVariantAndProperty(invariantVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -82,7 +82,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(ruleEn); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.true; done(); @@ -94,7 +94,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(ruleInv); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -106,7 +106,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(statePropAInv); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -117,7 +117,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(statePropAInv); manager - .isPermittedForVariantAndProperty(invariantVariant, propB) + .isPermittedForVariantAndProperty(invariantVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -129,7 +129,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(statePropAInv); manager - .isPermittedForVariantAndProperty(englishVariant, propA) + .isPermittedForVariantAndProperty(englishVariant, propA, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -141,7 +141,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(rulePlain); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.true; done(); @@ -154,7 +154,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(ruleNoEn); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -166,7 +166,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(ruleNoPlain); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -179,7 +179,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(ruleEn); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -193,7 +193,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(ruleNoEn); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -206,7 +206,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(rulePlain); manager - .isPermittedForVariantAndProperty(englishVariant, propB) + .isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -220,7 +220,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(statePropAInv); manager - .isPermittedForVariantAndProperty(invariantVariant, propA) + .isPermittedForVariantAndProperty(invariantVariant, propA, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); @@ -234,7 +234,7 @@ describe('UmbVariantPropertyGuardManager', () => { manager.addRule(stateNoPropAInv); manager - .isPermittedForVariantAndProperty(invariantVariant, propA) + .isPermittedForVariantAndProperty(invariantVariant, propA, invariantVariant) .subscribe((value) => { expect(value).to.be.false; done(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.ts index fb2612f537..49a0a4c922 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.ts @@ -5,21 +5,39 @@ import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; import { UmbGuardManagerBase } from '@umbraco-cms/backoffice/utils'; export interface UmbVariantPropertyGuardRule extends UmbPropertyGuardRule { + /** + * @description - The variant id of the property. + * @type {UmbVariantId} + * @memberof UmbVariantPropertyGuardRule + */ variantId?: UmbVariantId; + + /** + * @description - The variant id of the dataset. This is used to determine if the rule applies to the current dataset. + * @type {UmbVariantId} + * @memberof UmbVariantPropertyGuardRule + */ + datasetVariantId?: UmbVariantId; } /** * - * @param rule - * @param variantId - * @param propertyType + * @param {UmbVariantPropertyGuardRule} rule - The rule to check. + * @param {UmbVariantId} variantId - The property variant id to check. + * @param {UmbReferenceByUnique} propertyType - The property type to check. + * @param {UmbVariantId} datasetVariantId - The variant id of the dataset. This is used to determine if the rule applies to the current dataset. + * @returns {boolean} - Returns true if the rule applies to the given conditions. */ -function findRule(rule: UmbVariantPropertyGuardRule, variantId: UmbVariantId, propertyType: UmbReferenceByUnique) { +function findRule( + rule: UmbVariantPropertyGuardRule, + variantId: UmbVariantId, + propertyType: UmbReferenceByUnique, + datasetVariantId: UmbVariantId, +) { return ( - (rule.variantId?.compare(variantId) && rule.propertyType?.unique === propertyType.unique) || - (rule.variantId === undefined && rule.propertyType?.unique === propertyType.unique) || - (rule.variantId?.compare(variantId) && rule.propertyType === undefined) || - (rule.variantId === undefined && rule.propertyType === undefined) + (rule.variantId === undefined || rule.variantId.culture === variantId.culture) && + (rule.propertyType === undefined || rule.propertyType.unique === propertyType.unique) && + (rule.datasetVariantId === undefined || rule.datasetVariantId.culture === datasetVariantId.culture) ); } @@ -34,33 +52,54 @@ export class UmbVariantPropertyGuardManager extends UmbGuardManagerBase} - Returns an observable that emits true if the variant and propertyType is permitted, false otherwise. * @memberof UmbVariantPropertyGuardManager */ - isPermittedForVariantAndProperty(variantId: UmbVariantId, propertyType: UmbReferenceByUnique): Observable { - return this._rules.asObservablePart((rules) => this.#resolvePermission(rules, variantId, propertyType)); + isPermittedForVariantAndProperty( + variantId: UmbVariantId, + propertyType: UmbReferenceByUnique, + datasetVariantId: UmbVariantId, + ): Observable { + return this._rules.asObservablePart((rules) => + this.#resolvePermission(rules, variantId, propertyType, datasetVariantId), + ); } /** * Checks if the variant and propertyType is permitted. * @param {UmbVariantId} variantId - The variant id to check. * @param {UmbReferenceByUnique} propertyType - The property type to check. + * @param {UmbVariantId} datasetVariantId - The dataset variant id to check. * @returns {boolean} - Returns true if the variant and propertyType is permitted, false otherwise. * @memberof UmbVariantPropertyGuardManager */ - getIsPermittedForVariantAndProperty(variantId: UmbVariantId, propertyType: UmbReferenceByUnique): boolean { - return this.#resolvePermission(this._rules.getValue(), variantId, propertyType); + getIsPermittedForVariantAndProperty( + variantId: UmbVariantId, + propertyType: UmbReferenceByUnique, + datasetVariantId: UmbVariantId, + ): boolean { + return this.#resolvePermission(this._rules.getValue(), variantId, propertyType, datasetVariantId); } #resolvePermission( rules: UmbVariantPropertyGuardRule[], variantId: UmbVariantId, propertyType: UmbReferenceByUnique, + datasetVariantId: UmbVariantId, ) { - if (rules.filter((x) => x.permitted === false).some((rule) => findRule(rule, variantId, propertyType))) { + if ( + rules + .filter((x) => x.permitted === false) + .some((rule) => findRule(rule, variantId, propertyType, datasetVariantId)) + ) { return false; } - if (rules.filter((x) => x.permitted === true).some((rule) => findRule(rule, variantId, propertyType))) { + if ( + rules + .filter((x) => x.permitted === true) + .some((rule) => findRule(rule, variantId, propertyType, datasetVariantId)) + ) { return true; } return this._fallback; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/item/item-server-data-source-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/item/item-server-data-source-base.ts index 02df1f5429..8e3f1eeea5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/item/item-server-data-source-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/item/item-server-data-source-base.ts @@ -1,10 +1,11 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbDataSourceResponse } from '../data-source-response.interface.js'; import type { UmbItemDataSource } from './item-data-source.interface.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { tryExecute } from '@umbraco-cms/backoffice/resources'; export interface UmbItemServerDataSourceBaseArgs { - getItems: (uniques: Array) => Promise>>; + getItems?: (uniques: Array) => Promise>>; mapper: (item: ServerItemType) => ClientItemType; } @@ -14,10 +15,10 @@ export interface UmbItemServerDataSourceBaseArgs + extends UmbControllerBase implements UmbItemDataSource { - #host: UmbControllerHost; - #getItems: (uniques: Array) => Promise>>; + #getItems?: (uniques: Array) => Promise>>; #mapper: (item: ServerItemType) => ClientItemType; /** @@ -27,7 +28,7 @@ export abstract class UmbItemServerDataSourceBase) { - this.#host = host; + super(host); this.#getItems = args.getItems; this.#mapper = args.mapper; } @@ -39,14 +40,17 @@ export abstract class UmbItemServerDataSourceBase) { + if (!this.#getItems) throw new Error('getItems is not implemented'); if (!uniques) throw new Error('Uniques are missing'); - const { data, error } = await tryExecute(this.#host, this.#getItems(uniques)); - if (data) { - const items = data.map((item) => this.#mapper(item)); - return { data: items }; - } + const { data, error } = await tryExecute(this, this.#getItems(uniques)); - return { error }; + return { data: this._getMappedItems(data), error }; + } + + protected _getMappedItems(items: Array | undefined): Array | undefined { + if (!items) return undefined; + if (!this.#mapper) throw new Error('Mapper is not implemented'); + return items.map((item) => this.#mapper(item)); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts index 72663e8ad0..7d31d0ad83 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts @@ -33,10 +33,10 @@ export class UmbRepositoryDetailsManager return this.#init; } - #uniques = new UmbArrayState([], (x) => x); + #uniques = new UmbArrayState([], (x) => x); uniques = this.#uniques.asObservable(); - #entries = new UmbArrayState([], (x) => x.unique); + #entries = new UmbArrayState([], (x) => x.unique); entries = this.#entries.asObservable(); #statuses = new UmbArrayState([], (x) => x.unique); @@ -77,11 +77,15 @@ export class UmbRepositoryDetailsManager this.uniques, (uniques) => { // remove entries based on no-longer existing uniques: - const removedEntries = this.#entries.getValue().filter((entry) => !uniques.includes(entry.unique)); - this.#entries.remove(removedEntries); + const removedEntries = this.#entries + .getValue() + .filter((entry) => !uniques.includes(entry.unique)) + .map((x) => x.unique); + this.#statuses.remove(removedEntries); + this.#entries.remove(removedEntries); removedEntries.forEach((entry) => { - this.removeUmbControllerByAlias('observeEntry_' + entry.unique); + this.removeUmbControllerByAlias('observeEntry_' + entry); }); this.#requestNewDetails(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/api-interceptor.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/api-interceptor.controller.ts index 7e84e24d0c..00c5a524c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/api-interceptor.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/api-interceptor.controller.ts @@ -16,6 +16,7 @@ export class UmbApiInterceptorController extends UmbControllerBase { this.addAuthResponseInterceptor(client); this.addUmbGeneratedResourceInterceptor(client); this.addUmbNotificationsInterceptor(client); + this.addForbiddenResponseInterceptor(client); this.addErrorInterceptor(client); } @@ -38,6 +39,24 @@ export class UmbApiInterceptorController extends UmbControllerBase { }); } + /** + * Interceptor which checks responses for 403 errors and displays them as a notification. + * @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors. + * @internal + */ + addForbiddenResponseInterceptor(client: typeof umbHttpClient) { + client.interceptors.response.use(async (response: Response) => { + if (response.status === 403) { + const headline = 'Permission Denied'; + const message = 'You do not have the necessary permissions to complete the requested action. If you believe this is in error, please reach out to your administrator.'; + + this.#peekError(headline, message, null); + } + + return response; + }); + } + /** * Interceptor which checks responses for the Umb-Generated-Resource header and replaces the value into the response body. * @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/data-api/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/data-api/types.ts new file mode 100644 index 0000000000..dfade83718 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/data-api/types.ts @@ -0,0 +1,3 @@ +export interface UmbDataApiResponse { + data: ResponseType['data']; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts index e1e1756111..04e74539c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/index.ts @@ -1,12 +1,9 @@ export * from './api-interceptor.controller.js'; -export * from './resource.controller.js'; -export * from './try-execute.controller.js'; -export * from './tryExecute.function.js'; -export * from './tryExecuteAndNotify.function.js'; -export * from './tryXhrRequest.function.js'; -export * from './extractUmbNotificationColor.function.js'; -export * from './extractUmbColorVariable.function.js'; -export * from './isUmbNotifications.function.js'; export * from './apiTypeValidators.function.js'; +export * from './extractUmbColorVariable.function.js'; +export * from './extractUmbNotificationColor.function.js'; +export * from './isUmbNotifications.function.js'; +export * from './resource.controller.js'; +export * from './try-execute/index.js'; export * from './umb-error.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/batch-try-execute.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/batch-try-execute.function.ts new file mode 100644 index 0000000000..47a810ceb8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/batch-try-execute.function.ts @@ -0,0 +1,17 @@ +import { tryExecute } from './tryExecute.function.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +/** + * Batches promises and returns a promise that resolves to an array of results + * @param {UmbControllerHost} host - The host to use for the request and where notifications will be shown + * @param {Array>} chunks - The array of chunks to process + * @param {(chunk: Array) => Promise} callback - The function to call for each chunk + * @returns {Promise[]>} - A promise that resolves to an array of results + */ +export function batchTryExecute( + host: UmbControllerHost, + chunks: Array>, + callback: (chunk: Array) => Promise, +): Promise[]> { + return Promise.allSettled(chunks.map((chunk) => tryExecute(host, callback(chunk), { disableNotifications: true }))); +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/index.ts new file mode 100644 index 0000000000..18d2ee9f97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/index.ts @@ -0,0 +1,5 @@ +export * from './batch-try-execute.function.js'; +export * from './try-execute.controller.js'; +export * from './tryExecute.function.js'; +export * from './tryExecuteAndNotify.function.js'; +export * from './tryXhrRequest.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/try-execute.controller.ts similarity index 86% rename from src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute.controller.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/try-execute.controller.ts index 353c081138..afca34ad55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/try-execute.controller.ts @@ -1,7 +1,7 @@ -import { isProblemDetailsLike } from './apiTypeValidators.function.js'; -import { UmbResourceController } from './resource.controller.js'; -import type { UmbApiResponse, UmbTryExecuteOptions } from './types.js'; -import { UmbApiError, UmbCancelError } from './umb-error.js'; +import { isProblemDetailsLike } from '../apiTypeValidators.function.js'; +import { UmbResourceController } from '../resource.controller.js'; +import type { UmbApiResponse, UmbTryExecuteOptions } from '../types.js'; +import { UmbApiError, UmbCancelError } from '../umb-error.js'; export class UmbTryExecuteController extends UmbResourceController { #abortSignal?: AbortSignal; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryExecute.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryExecute.function.ts similarity index 93% rename from src/Umbraco.Web.UI.Client/src/packages/core/resources/tryExecute.function.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryExecute.function.ts index 07c4b26cfd..7deca7c0e4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryExecute.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryExecute.function.ts @@ -1,5 +1,5 @@ +import type { UmbApiResponse, UmbTryExecuteOptions } from '../types.js'; import { UmbTryExecuteController } from './try-execute.controller.js'; -import type { UmbApiResponse, UmbTryExecuteOptions } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryExecuteAndNotify.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryExecuteAndNotify.function.ts similarity index 96% rename from src/Umbraco.Web.UI.Client/src/packages/core/resources/tryExecuteAndNotify.function.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryExecuteAndNotify.function.ts index 1708d759c0..6bddf902de 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryExecuteAndNotify.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryExecuteAndNotify.function.ts @@ -1,5 +1,5 @@ +import type { UmbApiResponse } from '../types.js'; import { UmbTryExecuteController } from './try-execute.controller.js'; -import type { UmbApiResponse } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryXhrRequest.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts similarity index 94% rename from src/Umbraco.Web.UI.Client/src/packages/core/resources/tryXhrRequest.function.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts index a2e96d5d40..2be9e84fa7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryXhrRequest.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts @@ -1,8 +1,8 @@ +import { UmbCancelablePromise } from '../cancelable-promise.js'; +import { UmbApiError } from '../umb-error.js'; +import { isProblemDetailsLike } from '../apiTypeValidators.function.js'; +import type { UmbApiResponse, XhrRequestOptions } from '../types.js'; import { UmbTryExecuteController } from './try-execute.controller.js'; -import { UmbCancelablePromise } from './cancelable-promise.js'; -import { UmbApiError } from './umb-error.js'; -import { isProblemDetailsLike } from './apiTypeValidators.function.js'; -import type { UmbApiResponse, XhrRequestOptions } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { umbHttpClient } from '@umbraco-cms/backoffice/http-client'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts index fd7413e8f1..6a438f0bab 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts @@ -1,4 +1,5 @@ import type { UmbApiError, UmbCancelError, UmbError } from './umb-error.js'; +export type * from './data-api/types.js'; export interface XhrRequestOptions extends UmbTryExecuteOptions { baseUrl?: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.test.ts index 64c5db6c34..2d797072c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.test.ts @@ -46,7 +46,7 @@ describe('UmbSorterController', () => { beforeEach(async () => { element = await fixture(html``); - await aTimeout(10); + //await aTimeout(10); }); it('is defined with its own instance', () => { @@ -104,8 +104,8 @@ describe('UmbSorterController', () => { expect(element.sorter).to.have.property('notifyDisallowed').that.is.a('function'); }); - it('has a notifyRequestDrop method', () => { - expect(element.sorter).to.have.property('notifyRequestDrop').that.is.a('function'); + it('has a notifyRequestMove method', () => { + expect(element.sorter).to.have.property('notifyRequestMove').that.is.a('function'); }); it('has a destroy method', () => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts index 9382acd2eb..8b7546a4fc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts @@ -7,8 +7,9 @@ const autoScrollSpeed = 16; /** * - * @param el - * @param includeSelf + * @param {Element} el - The element to check for ability to scroll + * @param {Boolean} includeSelf - If true, the element itself will be included in the check + * @returns {Element | null} */ function getParentScrollElement(el: Element, includeSelf: boolean) { if (!el || !el.getBoundingClientRect) return null; @@ -45,8 +46,8 @@ function getParentScrollElement(el: Element, includeSelf: boolean) { /** * - * @param element - * @param ignorerSelectors + * @param {HTMLElement} element - The element to check + * @param {string} ignorerSelectors - A comma separated list of selectors to ignore */ function setupIgnorerElements(element: HTMLElement, ignorerSelectors: string) { const selectors = ignorerSelectors.split(','); @@ -57,8 +58,8 @@ function setupIgnorerElements(element: HTMLElement, ignorerSelectors: string) { } /** * - * @param element - * @param ignorerSelectors + * @param {HTMLElement} element - The element to check + * @param {string} ignorerSelectors - A comma separated list of selectors to ignore */ function destroyIgnorerElements(element: HTMLElement, ignorerSelectors: string) { const selectors = ignorerSelectors.split(','); @@ -69,7 +70,7 @@ function destroyIgnorerElements(element: HTMLElement, ignorerSelectors: string) } /** * - * @param element + * @param {Element} element - The element to check */ function setupPreventEvent(element: Element) { (element as HTMLElement).draggable = false; @@ -77,7 +78,7 @@ function setupPreventEvent(element: Element) { } /** * - * @param element + * @param {Element} element - The element to check */ function destroyPreventEvent(element: Element) { (element as HTMLElement).draggable = false; @@ -97,7 +98,7 @@ export type UmbSorterResolvePlacementArgs = { /** * This callback is executed when an item is moved from another container to this container. */ - onContainerChange?: (argument: { item: T; model: Array; from: UmbSorterController }) => void; + onContainerChange?: (argument: { + item: T; + model: Array; + from: UmbSorterController | undefined; + }) => void; onEnd?: (argument: { item: T; element: ElementType }) => void; itemHasNestedContainersResolver?: (element: HTMLElement) => boolean; /** @@ -190,6 +195,18 @@ type INTERNAL_UmbSorterConfig = { * Callback when user tries to move an item from another Sorter to this Sorter, return true or false to allow or disallow the move. */ onRequestMove?: (argument: { item: T }) => boolean; + /** + * Callback when user tries to drop an item from another window/tab/source. + */ + onRequestDrop?: (argument: { unique: string }) => Promise; + /** + * Callback when user tries to remove an item from another Sorter to this Sorter, return true or false to allow or disallow the move. + */ + requestExternalRemove?: (argument: { item: T }) => Promise; + /** + * Callback when user tries to remove an item from another Sorter to this Sorter, return true or false to allow or disallow the move. + */ + requestExternalInsert?: (argument: { item: T }) => Promise; /** * This callback is executed when an item is hovered within this container. * The callback should return true if the item should be placed after the hovered item, or false if it should be placed before the hovered item. @@ -337,7 +354,7 @@ export class UmbSorterController} + * @returns {Array} The model of this sorter. * @memberof UmbSorterController */ getModel(): Array { @@ -376,6 +393,7 @@ export class UmbSorterController { if (this.#isConnected === false) return; + if (this.#containerElement) { + // This can happen, so no need to show an error as it seems to be happening in some cases. We will just reject. [NL] + //console.error('Container element already initialized', this.#containerElement); + return; + } - const containerEl = - (this.#config.containerSelector - ? this.#host.shadowRoot!.querySelector(this.#config.containerSelector) - : this.#host) ?? this.#host; + const containerEl = this.#config.containerSelector + ? this.#host.shadowRoot!.querySelector(this.#config.containerSelector) + : this.#host; + + if (!containerEl) { + if (this.#config.containerSelector) { + throw new Error( + `Sorter could not find the container element, using this query selector '${this.#config.containerSelector}'.`, + ); + } else { + throw new Error('Sorter could not get its host element.'); + } + return; + } this.#containerElement = containerEl as HTMLElement; this.#useContainerShadowRoot = this.#containerElement === this.#host; @@ -405,6 +438,7 @@ export class UmbSorterController)) { + UmbSorterController.activeSorter = undefined; + if (UmbSorterController.activeElement) { + this.#handleDragEnd(); + } + } + + if (UmbSorterController.dropSorter === (this as unknown as UmbSorterController)) { + // If we are the drop sorter, we can now remove out self to get into pure Native Drag n' drop. + UmbSorterController.dropSorter = undefined; + } + + if (UmbSorterController.lastIndicationSorter === (this as unknown as UmbSorterController)) { + // If we are the lastIndicationSorter, we can now remove out self to get into pure Native Drag n' drop. + UmbSorterController.lastIndicationSorter = undefined; + } + this.#observer.disconnect(); + + // For auto scroller: + this.#scrollElement = null; + if (this.#containerElement) { // Only look at the shadowRoot if the containerElement is host. const containerElement = this.#useContainerShadowRoot @@ -429,15 +484,75 @@ export class UmbSorterController this.destroyItem(item)); } - #itemDraggedOver = (e: DragEvent) => { - //if(UmbSorterController.activeSorter === this) return; + async #obtainIncomingItem(e: DragEvent) { + if ( + !UmbSorterController.dropSorter && + e.dataTransfer?.types.includes('text/umb-sorter-identifier#' + this.identifier.toString()) + ) { + // If we have no drop-sorter, and we share the same identifier, then we like to accept this drag. + const activeType: string | undefined = e.dataTransfer?.types.find((x) => + x.startsWith('text/umb-sorter-item-unique#'), + ); + if (activeType) { + const activeUnique = activeType.split('#')?.[1]; + + let activeItem = this.#model.find((x) => this.#config.getUniqueOfModel(x) === activeUnique); + if (activeItem) { + UmbSorterController.activeSorter = this as unknown as UmbSorterController; + } + // test if unique is already in the model: + if (!activeItem) { + // Find the active item: + activeItem = await this.#config.onRequestDrop?.({ unique: activeUnique }); + UmbSorterController.activeSorter = undefined; // Important as we use this to know if we can remove the item via these Sorter references or if it is a Native Drop. + if (!activeItem) { + // Then we assume this item was not part of this sorters scope. This is the spot where inserting a new item from dataTransfer could be implemented. + return false; + } + } + + if (this.hasItem(activeUnique)) { + return false; + } + + e.dataTransfer.setData('text/umb-sorter-item-accepted', 'true'); + + // Set states: + UmbSorterController.activeItem = activeItem; + UmbSorterController.activeElement = undefined; + UmbSorterController.activeDragElement = undefined; + UmbSorterController.dropSorter = this as unknown as UmbSorterController; + UmbSorterController.originalIndex = undefined; + UmbSorterController.originalSorter = undefined; + + //UmbSorterController.activeSorter = this as unknown as UmbSorterController; + //UmbSorterController.originalSorter = this as unknown as UmbSorterController; + window.addEventListener('mouseup', this.#handleMouseUp); + window.addEventListener('mouseout', this.#handleMouseUp); + window.addEventListener('mouseleave', this.#handleMouseUp); + window.addEventListener('mousemove', this.#handleMouseMove); + + if (!this.#scrollElement) { + this.#scrollElement = getParentScrollElement(this.#containerElement, true); + } + return true; + } + } + + return false; + } + + #itemDraggedOver = async (e: DragEvent) => { + const newDrop = await this.#obtainIncomingItem(e); const dropSorter = UmbSorterController.dropSorter as unknown as UmbSorterController; + if (!dropSorter || dropSorter.identifier !== this.identifier) return; if (dropSorter === this) { @@ -447,7 +562,7 @@ export class UmbSorterController { + this.#handleMoveEnd(); + }; + #getDraggableElement(element: HTMLElement) { if (this.#config.draggableSelector) { // Concept for enabling getting element within ShadowRoot: (But it might need to be configurable, so its still possible to get light dom element(slotted), despite the host is a web-component with shadow-dom.) [NL] @@ -509,6 +629,10 @@ export class UmbSorterController; // Notice, it is acceptable here to get index via object reference, but only cause there has been no change at this stage, otherwise we cannot trust the object instance is represented in the model — it could have mutated or been cloned [NL] - UmbSorterController.originalIndex = this.#model.indexOf(UmbSorterController.activeItem); - - if (!UmbSorterController.activeItem) { - console.error('Could not find item related to this element.', UmbSorterController.activeElement); - return; - } + UmbSorterController.originalIndex = this.#model.findIndex((x) => this.#config.getUniqueOfModel(x) === activeUnique); // Get the current index of the item: UmbSorterController.activeIndex = UmbSorterController.originalIndex; @@ -711,8 +841,11 @@ export class UmbSorterController { UmbSorterController.rqaId = undefined; - if (!UmbSorterController.activeElement || !UmbSorterController.activeItem) { + if (!UmbSorterController.activeItem) { return; } @@ -820,7 +962,7 @@ export class UmbSorterController this.#config.getUniqueOfModel(x) === activeUnique); if (activeIndex === -1) { activeIndex = null; } @@ -912,7 +1055,7 @@ export class UmbSorterController this.#config.getUniqueOfModel(x) !== itemUnique); + if (this.#model.length !== newModel.length) { this.#model = newModel; this.#config.onChange?.({ model: newModel, item }); return true; @@ -1118,7 +1261,7 @@ export class UmbSorterController x !== item).length > 0; } - public async moveItemInModel(newIndex: number, fromCtrl: UmbSorterController) { + public async moveItemInModel(newIndex: number, fromCtrl: UmbSorterController | undefined) { if (!UmbSorterController.activeItem) { console.error('There is no active item to move'); return false; @@ -1128,33 +1271,80 @@ export class UmbSorterController this.#config.getUniqueOfModel(x) === itemUnique) as T | undefined; + if (!item) { + return false; + } } - if (this.notifyRequestDrop({ item }) === false) { + // If we do not have a formCtrl, then it means that we dont know where it comes from, like via native drag across the Sorters awareness. + + if (this.notifyRequestMove({ item }) === false) { return false; } - const localMove = fromCtrl === (this as any); + let localMove = fromCtrl === (this as any); if (!localMove) { // Not a local move, so we have to switch container to continue: - if ((await fromCtrl.removeItem(item)) !== true) { - console.error('Sync could not remove item when moving to a new container'); - return false; - } + // Notice if fromCtrl is not defined this is properly a native drop. + if (fromCtrl) { + if ((await fromCtrl.removeItem(item)) !== true) { + console.error('Sorter could not remove item before moving to a new container'); + return false; + } + } else if (!fromCtrl) { + // Before we react to the external factor, lets see if we already got it in our model. + // Remove from the external model. + if (!this.#config.requestExternalRemove) { + console.error( + 'Sorter needs the requestExternalRemove to be defined, therefor we cannot drop the external item', + ); + return false; + } + UmbSorterController.activeSorter = this as unknown as UmbSorterController; + fromCtrl = this as unknown as UmbSorterController; + + if ((await this.#config.requestExternalRemove({ item })) !== true) { + console.error('Sorter could not remove the item before moving to a new container'); + return false; + } + if ((await this.#config.requestExternalInsert?.({ item })) !== true) { + console.error('Sorter could not insert the item into the new container'); + return false; + } + // This requestExternalInsert ^^ could have updated the model already. if so we should skip ahead to just move the item as a local move. + if (this.#model.find((x) => this.#config.getUniqueOfModel(x) === itemUnique)) { + localMove = true; + } + } + } + if (!localMove) { if (this.#config.performItemInsert) { const result = await this.#config.performItemInsert({ item, newIndex }); if (result === false) { console.error('Sync could not insert after a move a new container'); return false; } + + // If everything went well, we can set the new activeSorter (and dropSorter) to this, as we are switching container. [NL] + UmbSorterController.activeSorter = this as unknown as UmbSorterController; + UmbSorterController.dropSorter = this as unknown as UmbSorterController; + UmbSorterController.activeIndex = + this.#model.findIndex((x) => this.#config.getUniqueOfModel(x) === itemUnique) ?? 0; } else { const newModel = [...this.#model]; newModel.splice(newIndex, 0, item); @@ -1163,7 +1353,7 @@ export class UmbSorterController, + from: fromCtrl as unknown as UmbSorterController | undefined, }); this.#config.onChange?.({ model: newModel, item }); @@ -1178,7 +1368,7 @@ export class UmbSorterController this.#config.getUniqueOfModel(x) === itemUnique); if (oldIndex === -1) { console.error('Could not find item in model when performing internal move', this.getHostElement(), this.#model); return false; @@ -1213,7 +1403,7 @@ export class UmbSorterController; - if (this.notifyRequestDrop({ item: item }) === true) { + if (this.notifyRequestMove({ item: item }) === true) { this.notifyAllowed(); return true; } @@ -1305,27 +1495,10 @@ export class UmbSorterController} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts index 3f3ac52aa9..6e4c7cc03f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts @@ -13,6 +13,6 @@ export class UmbTemporaryFileConfigServerDataSource { * Get the temporary file configuration. */ getConfig() { - return tryExecute(this.#host, TemporaryFileService.getTemporaryFileConfiguration()); + return tryExecute(this.#host, TemporaryFileService.getTemporaryFileConfiguration(), { disableNotifications: true }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts index 7c3251f276..5125f1bff5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts @@ -188,10 +188,20 @@ export class UmbTemporaryFileManager< return TemporaryFileStatus.CANCELLED; } - if (error instanceof UmbApiError && error.status === 413) { - // Special handling for when the request body is too large - const maxFileSizeGuestimate = parseInt(/(\d+) bytes/.exec(error.problemDetails.title)?.[1] ?? '0', 10); - this.#notifyOnFileSizeLimitExceeded(maxFileSizeGuestimate, item); + if (UmbApiError.isUmbApiError(error)) { + // Handle the error based on the status code + if (error.status === 413) { + // Special handling for when the request body is too large + const maxFileSizeGuestimate = parseInt(/(\d+) bytes/.exec(error.problemDetails.title)?.[1] ?? '0', 10); + this.#notifyOnFileSizeLimitExceeded(maxFileSizeGuestimate, item); + } else { + this.#notificationContext?.peek('danger', { + data: { + headline: this.#localization.term('errors_receivedErrorFromServer'), + message: error.problemDetails.title, + }, + }); + } } else { this.#notificationContext?.peek('danger', { data: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts index ed019eec8b..6ddd369c03 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts @@ -19,7 +19,8 @@ import { import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action'; import { UmbDeprecation, UmbPaginationManager, debounce } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; -import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; +import { UmbParentEntityContext, type UmbEntityModel, type UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; +import { ensureSlash } from '@umbraco-cms/backoffice/router'; export abstract class UmbTreeItemContextBase< TreeItemType extends UmbTreeItemModel, @@ -81,6 +82,7 @@ export abstract class UmbTreeItemContextBase< #actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; #hasChildrenContext = new UmbHasChildrenEntityContext(this); + #parentContext = new UmbParentEntityContext(this); // TODO: get this from the tree context #paging = { @@ -144,6 +146,13 @@ export abstract class UmbTreeItemContextBase< this.#hasChildren.setValue(hasChildren); this.#hasChildrenContext.setHasChildren(hasChildren); + const parentEntity: UmbEntityModel | undefined = treeItem.parent + ? { + entityType: treeItem.parent.entityType, + unique: treeItem.parent.unique, + } + : undefined; + this.#parentContext.setParent(parentEntity); this._treeItem.setValue(treeItem); // Update observers: @@ -441,9 +450,12 @@ export abstract class UmbTreeItemContextBase< return; } - const path = this.#path.getValue(); - const location = window.location.pathname; - const isActive = location.includes(path); + /* Check if the current location includes the path of this tree item. + We ensure that the paths ends with a slash to avoid collisions with paths like /path-1 and /path-1-2 where /path-1 is in both. + Instead we compare /path-1/ with /path-1-2/ which wont collide.*/ + const location = ensureSlash(window.location.pathname); + const comparePath = ensureSlash(this.#path.getValue()); + const isActive = location.includes(comparePath); this.#isActive.setValue(isActive); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts index d5be31e98c..ee32a706f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts @@ -13,6 +13,7 @@ export abstract class UmbTreeItemElementBase< this._item = newVal; if (this._item) { + this._label = this.localize.string(this._item?.name ?? ''); this.#initTreeItem(); } } @@ -21,6 +22,9 @@ export abstract class UmbTreeItemElementBase< } protected _item?: TreeItemModelType; + @state() + _label?: string; + @property({ type: Object, attribute: false }) public set api(value: TreeItemContextType | undefined) { this.#api = value; @@ -119,7 +123,6 @@ export abstract class UmbTreeItemElementBase< // Note: Currently we want to prevent opening when the item is in a selectable context, but this might change in the future. // If we like to be able to open items in selectable context, then we might want to make it as a menu item action, so you have to click ... and chose an action called 'Edit' override render() { - const label = this.localize.string(this._item?.name ?? ''); return html` ${this.renderIconContainer()} ${this.renderLabel()} ${this.#renderActions()} ${this.#renderChildItems()} @@ -187,7 +190,7 @@ export abstract class UmbTreeItemElementBase< slot="actions" .entityType=${this.#api.entityType} .unique=${this.#api.unique} - .label=${this._item.name}> + .label=${this.localize.term('actions_viewActionsFor', [this._label])}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/batch-array.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/batch-array.test.ts new file mode 100644 index 0000000000..e958f3fea1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/batch-array.test.ts @@ -0,0 +1,25 @@ +import { expect } from '@open-wc/testing'; +import { batchArray } from './batch-array.js'; + +describe('batchArray', () => { + it('should split an array into chunks of the specified size', () => { + const array = [1, 2, 3, 4, 5]; + const batchSize = 2; + const result = batchArray(array, batchSize); + expect(result).to.deep.equal([[1, 2], [3, 4], [5]]); + }); + + it('should handle arrays smaller than the batch size', () => { + const array = [1]; + const batchSize = 2; + const result = batchArray(array, batchSize); + expect(result).to.deep.equal([[1]]); + }); + + it('should handle empty arrays', () => { + const array: number[] = []; + const batchSize = 2; + const result = batchArray(array, batchSize); + expect(result).to.deep.equal([]); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/batch-array.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/batch-array.ts new file mode 100644 index 0000000000..fde5b6703d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/batch-array.ts @@ -0,0 +1,16 @@ +/** + * Splits an array into chunks of a specified size + * @param { Array } array - The array to split + * @param {number }batchSize - The size of each chunk + * @returns {Array>} - An array of chunks + */ +export function batchArray( + array: Array, + batchSize: number, +): Array> { + const chunks: Array> = []; + for (let i = 0; i < array.length; i += batchSize) { + chunks.push(array.slice(i, i + batchSize)); + } + return chunks; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/index.ts new file mode 100644 index 0000000000..106dce2d85 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/array/index.ts @@ -0,0 +1 @@ +export * from './batch-array.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts index aa191db2e7..a59e5dc990 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts @@ -1,3 +1,4 @@ +export * from './array/index.js'; export * from './bytes/bytes.function.js'; export * from './debounce/debounce.function.js'; export * from './deprecation/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.function.ts index 8958da1c0b..c5ee719cfc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.function.ts @@ -1,43 +1,39 @@ /** * Get the dimensions of an image from a URL. * @param {string} url The URL of the image. It can be a local file (blob url) or a remote file. - * @param {{maxWidth?: number}} opts Options for the image size. - * @param {number} opts.maxWidth The maximum width of the image. If the image is wider than this, it will be scaled down to this width while keeping the aspect ratio. - * @returns {Promise<{width: number, height: number, naturalWidth: number, naturalHeight: number}>} The width and height of the image as downloaded from the URL. The width and height can differ from the natural numbers if maxImageWidth is given. + * @param {{maxWidth?: number, maxHeight?: number}} opts Options for the image size. + * @param {number} opts.maxWidth The maximum width of the image. + * @param {number} opts.maxHeight The maximum height of the image. + * @returns {Promise<{width: number, height: number, naturalWidth: number, naturalHeight: number}>} The dimensions of the image. */ export function imageSize( url: string, - opts?: { maxWidth?: number }, + opts?: { maxWidth?: number; maxHeight?: number }, ): Promise<{ width: number; height: number; naturalWidth: number; naturalHeight: number }> { const img = new Image(); const promise = new Promise<{ width: number; height: number; naturalWidth: number; naturalHeight: number }>( (resolve, reject) => { img.onload = () => { - // Natural size is the actual image size regardless of rendering. - // The 'normal' `width`/`height` are for the **rendered** size. const naturalWidth = img.naturalWidth; const naturalHeight = img.naturalHeight; let width = naturalWidth; let height = naturalHeight; - if (opts?.maxWidth && opts.maxWidth > 0 && width > opts?.maxWidth) { - const ratio = opts.maxWidth / naturalWidth; - width = opts.maxWidth; + if ((opts?.maxWidth && opts.maxWidth > 0) || (opts?.maxHeight && opts.maxHeight > 0)) { + const widthRatio = opts?.maxWidth ? opts.maxWidth / naturalWidth : 1; + const heightRatio = opts?.maxHeight ? opts.maxHeight / naturalHeight : 1; + const ratio = Math.min(widthRatio, heightRatio, 1); // Never upscale + width = Math.round(naturalWidth * ratio); height = Math.round(naturalHeight * ratio); } - // Resolve promise with the width and height resolve({ width, height, naturalWidth, naturalHeight }); }; - - // Reject promise on error img.onerror = reject; }, ); - // Setting the source makes it start downloading and eventually call `onload` img.src = url; - return promise; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.test.ts new file mode 100644 index 0000000000..57b9cdbd36 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.test.ts @@ -0,0 +1,83 @@ +import { imageSize } from './image-size.function'; +import { expect } from '@open-wc/testing'; + +describe('imageSize', () => { + let OriginalImage: typeof Image; + + before(() => { + OriginalImage = window.Image; + }); + + after(() => { + window.Image = OriginalImage; + }); + + function mockImage(naturalWidth: number, naturalHeight: number) { + class MockImage { + naturalWidth = naturalWidth; + naturalHeight = naturalHeight; + onload: (() => void) | null = null; + onerror: (() => void) | null = null; + set src(_url: string) { + setTimeout(() => this.onload && this.onload(), 0); + } + } + // @ts-ignore + window.Image = MockImage; + } + + it('returns natural size if no maxWidth or maxHeight is given', async () => { + mockImage(800, 600); + const result = await imageSize('fake-url'); + expect(result).to.deep.equal({ + width: 800, + height: 600, + naturalWidth: 800, + naturalHeight: 600, + }); + }); + + it('scales down to maxWidth and maxHeight, ratio locked', async () => { + mockImage(800, 600); + const result = await imageSize('fake-url', { maxWidth: 400, maxHeight: 300 }); + expect(result).to.deep.equal({ + width: 400, + height: 300, + naturalWidth: 800, + naturalHeight: 600, + }); + }); + + it('never upscales if maxWidth/maxHeight are larger than natural', async () => { + mockImage(800, 600); + const result = await imageSize('fake-url', { maxWidth: 1000, maxHeight: 1000 }); + expect(result).to.deep.equal({ + width: 800, + height: 600, + naturalWidth: 800, + naturalHeight: 600, + }); + }); + + it('scales down by width if width is limiting', async () => { + mockImage(800, 600); + const result = await imageSize('fake-url', { maxWidth: 400 }); + expect(result).to.deep.equal({ + width: 400, + height: 300, + naturalWidth: 800, + naturalHeight: 600, + }); + }); + + it('scales down by height if height is limiting', async () => { + mockImage(800, 600); + const result = await imageSize('fake-url', { maxHeight: 150 }); + expect(result).to.deep.equal({ + width: 200, + height: 150, + naturalWidth: 800, + naturalHeight: 600, + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/bind-server-validation-to-form-control.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/bind-server-validation-to-form-control.controller.ts index 0c2593fd78..cb93af9066 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/bind-server-validation-to-form-control.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/bind-server-validation-to-form-control.controller.ts @@ -31,7 +31,7 @@ export class UmbBindServerValidationToFormControl extends UmbControllerBase { this.#value = value; // Only remove server validations from validation context [NL] const toRemove = this.#messages.filter((x) => x.type === 'server').map((msg) => msg.key); - this.#context?.messages.removeMessageByKeys(toRemove); + this.#context?.messages?.removeMessageByKeys(toRemove); } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/form-control-validator.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/form-control-validator.controller.ts index 67bc5523ec..8a1e5282c9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/form-control-validator.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/form-control-validator.controller.ts @@ -30,7 +30,7 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal this.#context = context; context?.addValidator(this); // If we have a message already, then un-pristine the control: - if (dataPath && context?.messages.getHasMessagesOfPathAndDescendant(dataPath)) { + if (dataPath && context?.messages?.getHasMessagesOfPathAndDescendant(dataPath)) { formControl.pristine = false; } }); @@ -46,11 +46,11 @@ export class UmbFormControlValidator extends UmbControllerBase implements UmbVal if (this.#dataPath) { if (newVal) { - this.#context?.messages.removeMessagesByTypeAndPath('client', this.#dataPath); + this.#context?.messages?.removeMessagesByTypeAndPath('client', this.#dataPath); } else { // We only want to add the message if it is not already there. (this could be a custom or server message that got binded to the control, we do not want that double.) - if (!this.#context?.messages.getHasMessageOfPathAndBody(this.#dataPath, this.#control.validationMessage)) { - this.#context?.messages.addMessage('client', this.#dataPath, this.#control.validationMessage); + if (!this.#context?.messages?.getHasMessageOfPathAndBody(this.#dataPath, this.#control.validationMessage)) { + this.#context?.messages?.addMessage('client', this.#dataPath, this.#control.validationMessage); } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts index 821324469a..e57bd98a68 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts @@ -27,7 +27,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal >; #inUnprovidingState: boolean = false; - // @reprecated - Will be removed in v.17 + // @deprecated - Will be removed in v.17 // Local version of the data send to the server, only use-case is for translation. #translationData = new UmbObjectState(undefined); /** @@ -81,7 +81,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal setVariantId(variantId: UmbVariantId): void { this.#variantId = variantId; // @.culture == null && @.segment == null - this.messages.filter((msg) => { + this.messages?.filter((msg) => { // Figure out how many times '@.culture ==' is present in the path: //const cultureMatches = (msg.path.match(/@\.culture ==/g) || []); // I like a Regex that finds all the @.culture == and @.segment == in the path. they are adjacent. and I like to know the value following '== ' @@ -113,7 +113,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal * @param translator */ async addTranslator(translator: UmbValidationMessageTranslator) { - this.messages.addTranslator(translator); + this.messages?.addTranslator(translator); } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/constants.ts new file mode 100644 index 0000000000..9c6a2415c8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/constants.ts @@ -0,0 +1 @@ +export * from './context/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/constants.ts new file mode 100644 index 0000000000..0b49906ceb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/constants.ts @@ -0,0 +1 @@ +export { UMB_VARIANT_CONTEXT } from './variant.context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/index.ts new file mode 100644 index 0000000000..85a9b48dce --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/index.ts @@ -0,0 +1 @@ +export { UmbVariantContext } from './variant.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.token.ts new file mode 100644 index 0000000000..d2733dac1b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.token.ts @@ -0,0 +1,4 @@ +import type { UmbVariantContext } from './variant.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_VARIANT_CONTEXT = new UmbContextToken('UmbVariantContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.ts new file mode 100644 index 0000000000..a71881beed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.ts @@ -0,0 +1,164 @@ +import { UmbVariantId } from '../variant-id.class.js'; +import { UMB_VARIANT_CONTEXT } from './variant.context.token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { mergeObservables, UmbClassState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; + +/** + * A context for the current variant state. + * @class UmbVariantContext + * @augments {UmbContextBase} + * @implements {UmbVariantContext} + */ +export class UmbVariantContext extends UmbContextBase { + #variantId = new UmbClassState(undefined); + public readonly variantId = this.#variantId.asObservable(); + public readonly culture = this.#variantId.asObservablePart((x) => x?.culture); + public readonly segment = this.#variantId.asObservablePart((x) => x?.segment); + + #fallbackCulture = new UmbStringState(undefined); + public fallbackCulture = this.#fallbackCulture.asObservable(); + + #appCulture = new UmbStringState(undefined); + public appCulture = this.#appCulture.asObservable(); + + public readonly displayCulture = mergeObservables([this.culture, this.appCulture], ([culture, appCulture]) => { + return culture ?? appCulture; + }); + + constructor(host: UmbControllerHost) { + super(host, UMB_VARIANT_CONTEXT); + } + + /** + * Inherit values from the parent variant context + * @returns {UmbVariantContext} - The current instance of the context + * @memberof UmbVariantContext + */ + inherit(): UmbVariantContext { + this.consumeContext(UMB_VARIANT_CONTEXT, (context) => { + this.observe( + context?.fallbackCulture, + (fallbackCulture) => { + if (!fallbackCulture) return; + this.#fallbackCulture.setValue(fallbackCulture); + }, + 'observeFallbackCulture', + ); + + this.observe( + context?.appCulture, + (appCulture) => { + if (!appCulture) return; + this.#appCulture.setValue(appCulture); + }, + 'observeAppCulture', + ); + }).skipHost(); + + return this; + } + + /** + * Sets the variant id state + * @param {UmbVariantId | undefined} variantId - The variant to set + * @memberof UmbVariantContext + */ + async setVariantId(variantId: UmbVariantId | undefined): Promise { + this.#variantId.setValue(variantId); + } + + /** + * Gets variant state + * @returns {Promise} - The variant state + * @memberof UmbVariantContext + */ + async getVariantId(): Promise { + return this.#variantId.getValue(); + } + + /** + * Gets the culture state + * @returns {(Promise)} - The culture state + * @memberof UmbVariantContext + */ + async getCulture(): Promise { + return this.#variantId.getValue()?.culture; + } + + /** + * Sets the variant culture state + * @param {string | undefined} culture - The culture to set + * @memberof UmbVariantContext + */ + async setCulture(culture: string | null): Promise { + const variantId = new UmbVariantId(culture, this.#variantId.getValue()?.segment); + this.#variantId.setValue(variantId); + } + + /** + * Gets the variant segment state + * @returns {(Promise)} - The segment state + * @memberof UmbVariantContext + */ + async getSegment(): Promise { + return this.#variantId.getValue()?.segment; + } + + /** + * Sets the variant segment state + * @param {string | undefined} segment - The segment to set + * @memberof UmbVariantContext + */ + async setSegment(segment: string | null): Promise { + const variantId = new UmbVariantId(this.#variantId.getValue()?.culture, segment); + this.#variantId.setValue(variantId); + } + + /** + * Gets the fallback culture state + * @returns {(Promise)} - The fallback culture state + * @memberof UmbVariantContext + */ + async getFallbackCulture(): Promise { + return this.#fallbackCulture.getValue(); + } + + /** + * Sets the fallback culture state + * @param {string | undefined} culture - The fallback culture to set + * @memberof UmbVariantContext + */ + async setFallbackCulture(culture: string | null): Promise { + this.removeUmbControllerByAlias('observeFallbackCulture'); + this.#fallbackCulture.setValue(culture); + } + + /** + * Gets the app culture state + * @returns {(Promise)} - The app culture state + * @memberof UmbVariantContext + */ + async getAppCulture(): Promise { + return this.#appCulture.getValue(); + } + + /** + * Sets the app culture state + * @param {string | undefined} culture - The app culture to set + * @memberof UmbVariantContext + */ + async setAppCulture(culture: string | null): Promise { + this.removeUmbControllerByAlias('observeAppCulture'); + this.#appCulture.setValue(culture); + } + + /** + * Gets the display culture state + * @returns {(Promise)} - The app culture state + * @memberof UmbVariantContext + */ + async getDisplayCulture(): Promise { + return this.observe(this.displayCulture).asPromise(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts index d190db617d..9d50648397 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts @@ -1,3 +1,5 @@ +export * from './constants.js'; +export * from './context/index.js'; export * from './variant-id.class.js'; export * from './variant-object-compare.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts index ffae6d8183..21b55ca72e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts @@ -3,7 +3,11 @@ import { css, customElement, html, ifDefined, map, state } from '@umbraco-cms/ba import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section'; -import type { UmbMenuStructureWorkspaceContext, UmbStructureItemModel } from '@umbraco-cms/backoffice/menu'; +import { + UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT, + type UmbMenuStructureWorkspaceContext, + type UmbStructureItemModel, +} from '@umbraco-cms/backoffice/menu'; @customElement('umb-workspace-breadcrumb') export class UmbWorkspaceBreadcrumbElement extends UmbLitElement { @@ -16,7 +20,7 @@ export class UmbWorkspaceBreadcrumbElement extends UmbLitElement { // TODO: figure out the correct context type #workspaceContext?: any; #sectionContext?: typeof UMB_SECTION_CONTEXT.TYPE; - #structureContext?: UmbMenuStructureWorkspaceContext; + #structureContext?: typeof UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT.TYPE; constructor() { super(); @@ -31,9 +35,7 @@ export class UmbWorkspaceBreadcrumbElement extends UmbLitElement { this.#observeName(); }); - // TODO: set up context token - this.consumeContext('UmbMenuStructureWorkspaceContext', (instance) => { - // TODO: get the correct interface from the context token + this.consumeContext(UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT, (instance) => { this.#structureContext = instance; this.#observeStructure(); }); @@ -46,9 +48,7 @@ export class UmbWorkspaceBreadcrumbElement extends UmbLitElement { this.observe( this.#structureContext.structure, (value) => { - // TODO: get the type from the context - const structure = value as Array; - this._structure = isNew ? structure : structure.slice(0, -1); + this._structure = isNew ? value : value.slice(0, -1); }, 'menuStructureObserver', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts index b554170121..aa770c7523 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts @@ -6,7 +6,10 @@ import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language'; import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section'; import type { UmbAppLanguageContext } from '@umbraco-cms/backoffice/language'; -import type { UmbVariantStructureItemModel } from '@umbraco-cms/backoffice/menu'; +import { + UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT, + type UmbVariantStructureItemModel, +} from '@umbraco-cms/backoffice/menu'; @customElement('umb-workspace-variant-menu-breadcrumb') export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { @@ -25,7 +28,7 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { #sectionContext?: typeof UMB_SECTION_CONTEXT.TYPE; #workspaceContext?: UmbVariantDatasetWorkspaceContext; #appLanguageContext?: UmbAppLanguageContext; - #structureContext?: any; + #structureContext?: typeof UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT.TYPE; constructor() { super(); @@ -46,8 +49,7 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { this.#observeStructure(); }); - // TODO: set up context token - this.consumeContext('UmbMenuStructureWorkspaceContext', (instance) => { + this.consumeContext(UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT, (instance) => { if (!instance) return; this.#structureContext = instance; this.#observeStructure(); @@ -59,9 +61,7 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { const isNew = this.#workspaceContext.getIsNew(); this.observe(this.#structureContext.structure, (value) => { - // TODO: get the type from the context - const structure = value as Array; - this._structure = isNew ? structure : structure.slice(0, -1); + this._structure = isNew ? value : value.slice(0, -1); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts index 4cb31ea938..4e88791029 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts @@ -299,6 +299,7 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< data-mark="input:entity-name" placeholder=${this.localize.term('placeholders_entername')} label=${this.localize.term('placeholders_entername')} + autocomplete="off" .value=${this.#getNameValue()} @input=${this.#handleInput} required diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts index 4982a046ad..6217bce75e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/index.ts @@ -1,14 +1,16 @@ export * from './entity-workspace.context-token.js'; export * from './publishable-workspace.context-token.js'; export * from './routable-workspace.context-token.js'; +export * from './submittable-tree-entity-workspace.context-token.js'; export * from './submittable-workspace.context-token.js'; export * from './saveable-workspace.context-token.js'; export * from './variant-workspace.context-token.js'; +export type * from '../../workspace-context.interface.js'; export type * from './entity-workspace-context.interface.js'; export type * from './invariant-dataset-workspace-context.interface.js'; export type * from './publishable-workspace-context.interface.js'; export type * from './routable-workspace-context.interface.js'; +export type * from './submittable-tree-entity-workspace-context.interface.js'; export type * from './submittable-workspace-context.interface.js'; export type * from './saveable-workspace-context.interface.js'; export type * from './variant-dataset-workspace-context.interface.js'; -export type * from '../../workspace-context.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/submittable-tree-entity-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/submittable-tree-entity-workspace-context.interface.ts new file mode 100644 index 0000000000..a47776c2ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/submittable-tree-entity-workspace-context.interface.ts @@ -0,0 +1,43 @@ +import type { UmbSubmittableWorkspaceContext } from './submittable-workspace-context.interface.js'; +import type { UmbEntityModel, UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; +import type { Observable } from '@umbraco-cms/backoffice/observable-api'; + +export interface UmbSubmittableTreeEntityWorkspaceContext extends UmbSubmittableWorkspaceContext { + /** + * The parent entity that the new entity will be created under. + * Internal property. Use UMB_PARENT_ENTITY_CONTEXT to get the parent entity. + * @type {(Observable)} + */ + _internal_createUnderParent: Observable; + + /** + * The entity type that the new entity will be created under. + * Internal property. Use UMB_PARENT_ENTITY_CONTEXT to get the parent entity. + * @type {(Observable)} + */ + _internal_createUnderParentEntityType: Observable; + + /** + * The entity unique that the new entity will be created under. + * Internal property. Use UMB_PARENT_ENTITY_CONTEXT to get the parent entity. + * @type {(Observable)} + */ + _internal_createUnderParentEntityUnique: Observable; + + /** + * The entity type that the new entity will be created under. + * Internal property. Use UMB_PARENT_ENTITY_CONTEXT to get the parent entity. + * @type {(Observable)} + */ + _internal_getCreateUnderParent(): UmbEntityModel | undefined; + + /** + * Sets the parent entity that the new entity will be created under. + * Internal property. Use UMB_PARENT_ENTITY_CONTEXT to get the parent entity. + * @param {UmbEntityModel | undefined} parent - The parent entity + */ + _internal_setCreateUnderParent(parent: UmbEntityModel | undefined): void; + + // TODO: This should be moved to the entity workspace context. It is added here to avoid a breaking change. + entityType: Observable; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/submittable-tree-entity-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/submittable-tree-entity-workspace.context-token.ts new file mode 100644 index 0000000000..95cc7765a5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/contexts/tokens/submittable-tree-entity-workspace.context-token.ts @@ -0,0 +1,13 @@ +import type { UmbWorkspaceContext } from '../../types.js'; +import type { UmbSubmittableTreeEntityWorkspaceContext } from './submittable-tree-entity-workspace-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT = new UmbContextToken< + UmbWorkspaceContext, + UmbSubmittableTreeEntityWorkspaceContext +>( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbSubmittableTreeEntityWorkspaceContext => + 'requestSubmit' in context && 'createUnderParent' in context, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts index 14262b5877..e7a76c4a56 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts @@ -1,5 +1,6 @@ import { UmbSubmittableWorkspaceContextBase } from '../submittable/index.js'; import { UmbEntityWorkspaceDataManager } from '../entity/entity-workspace-data-manager.js'; +import type { UmbSubmittableTreeEntityWorkspaceContext } from '../contexts/tokens/index.js'; import type { UmbEntityDetailWorkspaceContextArgs, UmbEntityDetailWorkspaceContextCreateArgs } from './types.js'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -25,11 +26,14 @@ import { UmbId } from '@umbraco-cms/backoffice/id'; const LOADING_STATE_UNIQUE = 'umbLoadingEntityDetail'; export abstract class UmbEntityDetailWorkspaceContextBase< - DetailModelType extends UmbEntityModel = UmbEntityModel, - DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository, - CreateArgsType extends - UmbEntityDetailWorkspaceContextCreateArgs = UmbEntityDetailWorkspaceContextCreateArgs, -> extends UmbSubmittableWorkspaceContextBase { + DetailModelType extends UmbEntityModel = UmbEntityModel, + DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository, + CreateArgsType extends + UmbEntityDetailWorkspaceContextCreateArgs = UmbEntityDetailWorkspaceContextCreateArgs, + > + extends UmbSubmittableWorkspaceContextBase + implements UmbSubmittableTreeEntityWorkspaceContext +{ // Just for context token safety: public readonly IS_ENTITY_DETAIL_WORKSPACE_CONTEXT = true; @@ -55,9 +59,27 @@ export abstract class UmbEntityDetailWorkspaceContextBase< #eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; - #parent = new UmbObjectState<{ entityType: string; unique: UmbEntityUnique } | undefined>(undefined); - public readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); - public readonly parentEntityType = this.#parent.asObservablePart((parent) => + #createUnderParent = new UmbObjectState(undefined); + _internal_createUnderParent = this.#createUnderParent.asObservable(); + + public readonly _internal_createUnderParentEntityUnique = this.#createUnderParent.asObservablePart((parent) => + parent ? parent.unique : undefined, + ); + + public readonly _internal_createUnderParentEntityType = this.#createUnderParent.asObservablePart((parent) => + parent ? parent.entityType : undefined, + ); + + /** + * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead to get the parent both when creating and editing. + */ + public readonly parentUnique = this.#createUnderParent.asObservablePart((parent) => + parent ? parent.unique : undefined, + ); + /** + * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead to get the parent both when creating and editing. + */ + public readonly parentEntityType = this.#createUnderParent.asObservablePart((parent) => parent ? parent.entityType : undefined, ); @@ -148,28 +170,56 @@ export abstract class UmbEntityDetailWorkspaceContextBase< this.#entityContext.setUnique(unique); } + /** + * Gets the parent that a new entity will be created under. + * @returns { UmbEntityModel | undefined } The parent entity + */ + _internal_getCreateUnderParent(): UmbEntityModel | undefined { + return this.#createUnderParent.getValue(); + } + + /** + * Sets the parent that a new entity will be created under. + * @param {UmbEntityModel} parent The parent entity + */ + _internal_setCreateUnderParent(parent: UmbEntityModel): void { + this.#createUnderParent.setValue(parent); + } + /** * Get the parent + * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead to get the parent both when creating and editing. * @returns { UmbEntityModel | undefined } The parent entity */ getParent(): UmbEntityModel | undefined { - return this.#parent.getValue(); + return this.#createUnderParent.getValue(); } + /** + * Set the parent + * @deprecated Will be removed in v.18. + * @param { UmbEntityModel } parent The parent entity + */ setParent(parent: UmbEntityModel) { - this.#parent.setValue(parent); + this.#createUnderParent.setValue(parent); } /** * Get the parent unique + * @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead to get the parent both when creating and editing. * @returns { string | undefined } The parent unique identifier */ getParentUnique(): UmbEntityUnique | undefined { - return this.#parent.getValue()?.unique; + return this.#createUnderParent.getValue()?.unique; } + /** + * Get the parent entity type + * @deprecated Will be removed in v.18 + * @returns { string | undefined } The parent entity type + */ getParentEntityType() { - return this.#parent.getValue()?.entityType; + return this.#createUnderParent.getValue()?.entityType; } async load( @@ -239,7 +289,9 @@ export abstract class UmbEntityDetailWorkspaceContextBase< this.resetState(); this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Creating ${this.getEntityType()} scaffold` }); await this.#init; + // keeping setParent for backwards compatibility. Remove in v18. this.setParent(args.parent); + this._internal_setCreateUnderParent(args.parent); const request = this._detailRepository!.createScaffold(args.preset); this._getDataPromise = request; @@ -281,7 +333,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< } if (this.getIsNew()) { - const parent = this.#parent.getValue(); + const parent = this.#createUnderParent.getValue(); if (parent?.unique === undefined) throw new Error('Parent unique is missing'); if (!parent.entityType) throw new Error('Parent entity type is missing'); await this._create(currentData, parent); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/workspace-info-app-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/workspace-info-app-layout.element.ts index 981a93651c..972d45755d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/workspace-info-app-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/workspace-info-app-layout.element.ts @@ -22,10 +22,6 @@ export class UmbWorkspaceInfoAppLayoutElement extends UmbLitElement { uui-box { --uui-box-default-padding: 0; } - - #container { - padding-left: var(--uui-size-space-4); - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts index 66fd7c0956..51ba76dc79 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts @@ -7,7 +7,7 @@ import type { import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantContext, UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbBooleanState, type Observable } from '@umbraco-cms/backoffice/observable-api'; /** @@ -23,6 +23,7 @@ export class UmbInvariantWorkspacePropertyDatasetContext< public readOnly = this.#readOnly.asObservable(); #workspace: WorkspaceType; + #variantContext = new UmbVariantContext(this).inherit(); name; @@ -49,6 +50,7 @@ export class UmbInvariantWorkspacePropertyDatasetContext< this.#workspace = workspace; this.name = this.#workspace.name; + this.#variantContext.setVariantId(this.getVariantId()); } get properties(): Observable | undefined> { diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/constants.ts index 6b39e48151..71e09a57d2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/constants.ts @@ -5,8 +5,10 @@ export * from './modals/constants.js'; export * from './paths.js'; export * from './reference/constants.js'; export * from './repository/constants.js'; +export * from './search/constants.js'; export * from './tree/constants.js'; export * from './workspace/constants.js'; + export { UMB_DATA_TYPE_ENTITY_TYPE, UMB_DATA_TYPE_ROOT_ENTITY_TYPE, diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.data-source.ts index d373f16b32..4b578c8b1d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.data-source.ts @@ -6,6 +6,7 @@ import { DataTypeService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; let manifestPropertyEditorUis: Array = []; @@ -26,7 +27,6 @@ export class UmbDataTypeItemServerDataSource extends UmbItemServerDataSourceBase constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); @@ -37,10 +37,21 @@ export class UmbDataTypeItemServerDataSource extends UmbItemServerDataSourceBase }) .unsubscribe(); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => DataTypeService.getItemDataType({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => DataTypeService.getItemDataType({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: DataTypeItemResponseModel): UmbDataTypeItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/search/constants.ts new file mode 100644 index 0000000000..c2d7478ea8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/search/constants.ts @@ -0,0 +1,2 @@ +export * from './global-search/constants.js'; +export const UMB_DATA_TYPE_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.DataType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/search/global-search/constants.ts new file mode 100644 index 0000000000..f6c707a62e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_DATA_TYPE_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.DataType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/search/global-search/manifests.ts new file mode 100644 index 0000000000..85f076e27e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/search/global-search/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_DATA_TYPE_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_DATA_TYPE_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; +import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; + +export const manifests: Array = [ + { + name: 'Data Type Global Search', + alias: UMB_DATA_TYPE_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 400, + meta: { + label: 'Data Types', + searchProviderAlias: UMB_DATA_TYPE_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_SETTINGS_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/search/manifests.ts index e02ff1c2a2..6c2c81365c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/search/manifests.ts @@ -1,4 +1,5 @@ import { UMB_DATA_TYPE_ENTITY_TYPE } from '../entity.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { @@ -17,4 +18,5 @@ export const manifests: Array = [ type: 'searchResultItem', forEntityTypes: [UMB_DATA_TYPE_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/tree-item-children/collection/views/data-type-tree-item-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/tree-item-children/collection/views/data-type-tree-item-table-collection-view.element.ts index 608d7e2ffc..09ee3671e6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/tree-item-children/collection/views/data-type-tree-item-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/tree-item-children/collection/views/data-type-tree-item-table-collection-view.element.ts @@ -94,6 +94,7 @@ export class UmbDataTypeTreeItemTableCollectionViewElement extends UmbLitElement .value=${{ entityType: item.entityType, unique: item.unique, + name: item.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/item/dictionary-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/item/dictionary-item.server.data-source.ts index c5b3714b91..245931dbfc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/item/dictionary-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/item/dictionary-item.server.data-source.ts @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { DictionaryItemItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DictionaryService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Dictionary items @@ -21,14 +22,24 @@ export class UmbDictionaryItemServerDataSource extends UmbItemServerDataSourceBa */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => DictionaryService.getItemDictionary({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => DictionaryService.getItemDictionary({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: DictionaryItemItemResponseModel): UmbDictionaryItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/constants.ts index b209cb74c4..ee327114e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/constants.ts @@ -1 +1,2 @@ +export * from './global-search/constants.js'; export const UMB_DICTIONARY_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Dictionary'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/global-search/constants.ts new file mode 100644 index 0000000000..ac2638ce13 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_DICTIONARY_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.Dictionary'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/global-search/manifests.ts new file mode 100644 index 0000000000..91856af6cb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/global-search/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_DICTIONARY_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_DICTIONARY_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; +import { UMB_TRANSLATION_SECTION_ALIAS } from '@umbraco-cms/backoffice/translation'; + +export const manifests: Array = [ + { + name: 'Dictionary Global Search', + alias: UMB_DICTIONARY_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 600, + meta: { + label: 'Dictionary', + searchProviderAlias: UMB_DICTIONARY_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_TRANSLATION_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/manifests.ts index 5c2f886811..ce4564264a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/search/manifests.ts @@ -1,5 +1,6 @@ import { UMB_DICTIONARY_ENTITY_TYPE } from '../entity.js'; import { UMB_DICTIONARY_SEARCH_PROVIDER_ALIAS } from './constants.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { @@ -18,4 +19,5 @@ export const manifests: Array = [ type: 'searchResultItem', forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/manifests.ts index 46a67c103b..b945f804e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/manifests.ts @@ -11,7 +11,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_DOCUMENT_BLUEPRINT_ROOT_ENTITY_TYPE, UMB_DOCUMENT_BLUEPRINT_FOLDER_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_createblueprint', + label: '#actions_create', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/document-blueprint-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/document-blueprint-item.server.data-source.ts index 104ecfbee1..bc44153a60 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/document-blueprint-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/document-blueprint-item.server.data-source.ts @@ -5,6 +5,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { DocumentBlueprintItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { tryExecute } from '@umbraco-cms/backoffice/resources'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A data source for Document Blueprint items that fetches data from the server @@ -15,7 +16,6 @@ export class UmbDocumentBlueprintItemServerDataSource extends UmbItemServerDataS DocumentBlueprintItemResponseModel, UmbDocumentBlueprintItemModel > { - #host: UmbControllerHost; /** * Creates an instance of UmbDocumentBlueprintItemServerDataSource. * @param {UmbControllerHost} host - The controller host for this controller to be appended to @@ -23,16 +23,14 @@ export class UmbDocumentBlueprintItemServerDataSource extends UmbItemServerDataS */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); - this.#host = host; } async getItemsByDocumentType(unique: string) { if (!unique) throw new Error('Unique is missing'); const { data, error } = await tryExecute( - this.#host, + this, DocumentTypeService.getDocumentTypeByIdBlueprint({ path: { id: unique } }), ); @@ -47,11 +45,21 @@ export class UmbDocumentBlueprintItemServerDataSource extends UmbItemServerDataS return { error }; } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => - DocumentBlueprintService.getItemDocumentBlueprint({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => DocumentBlueprintService.getItemDocumentBlueprint({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: DocumentBlueprintItemResponseModel): UmbDocumentBlueprintItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/item/document-type-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/item/document-type-item.server.data-source.ts index 1fed1d9574..f68f70d88c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/item/document-type-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/item/document-type-item.server.data-source.ts @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { DocumentTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DocumentTypeService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A data source for Document Type items that fetches data from the server @@ -20,13 +21,24 @@ export class UmbDocumentTypeItemServerDataSource extends UmbItemServerDataSource * @memberof UmbDocumentTypeItemServerDataSource */ constructor(host: UmbControllerHost) { - super(host, { getItems, mapper }); + super(host, { mapper }); + } + + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => DocumentTypeService.getItemDocumentType({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; } } -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => DocumentTypeService.getItemDocumentType({ query: { id: uniques } }); - const mapper = (item: DocumentTypeItemResponseModel): UmbDocumentTypeItemModel => { return { entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/constants.ts index ec598f76cc..eff2866ede 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/constants.ts @@ -1 +1,2 @@ +export * from './global-search/constants.js'; export const UMB_DOCUMENT_TYPE_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.DocumentType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/global-search/constants.ts new file mode 100644 index 0000000000..566c1b4fa2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_DOCUMENT_TYPE_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.DocumentType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/global-search/manifests.ts new file mode 100644 index 0000000000..1abbf41f52 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/global-search/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_DOCUMENT_TYPE_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_DOCUMENT_TYPE_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; +import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; + +export const manifests: Array = [ + { + name: 'Document Type Global Search', + alias: UMB_DOCUMENT_TYPE_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 600, + meta: { + label: 'Document Types', + searchProviderAlias: UMB_DOCUMENT_TYPE_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_SETTINGS_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/manifests.ts index 1fb211ba30..d84dab491f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/manifests.ts @@ -1,5 +1,6 @@ import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '../entity.js'; import { UMB_DOCUMENT_TYPE_SEARCH_PROVIDER_ALIAS } from './constants.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { @@ -19,4 +20,5 @@ export const manifests: Array = [ element: () => import('./document-type-search-result-item.element.js'), forEntityTypes: [UMB_DOCUMENT_TYPE_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/tree-item-children/collection/views/document-type-tree-item-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/tree-item-children/collection/views/document-type-tree-item-table-collection-view.element.ts index d2a698f37b..6717fc58c9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/tree-item-children/collection/views/document-type-tree-item-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/tree-item-children/collection/views/document-type-tree-item-table-collection-view.element.ts @@ -103,6 +103,7 @@ export class UmbDocumentTypeTreeItemTableCollectionViewElement extends UmbLitEle .value=${{ entityType: item.entityType, unique: item.unique, + name: item.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/document-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/document-collection.context.ts index 3e7ff1dafd..cff1aedd65 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/document-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/document-collection.context.ts @@ -2,13 +2,42 @@ import type { UmbDocumentCollectionFilterModel, UmbDocumentCollectionItemModel } import { UMB_DOCUMENT_TABLE_COLLECTION_VIEW_ALIAS } from './constants.js'; import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; +import { UmbStringState } from '@umbraco-cms/backoffice/observable-api'; export class UmbDocumentCollectionContext extends UmbDefaultCollectionContext< UmbDocumentCollectionItemModel, UmbDocumentCollectionFilterModel > { + #variantContext?: typeof UMB_VARIANT_CONTEXT.TYPE; + #displayCulture = new UmbStringState(undefined); + #displayCultureObservable = this.#displayCulture.asObservable(); + constructor(host: UmbControllerHost) { super(host, UMB_DOCUMENT_TABLE_COLLECTION_VIEW_ALIAS); + + this.consumeContext(UMB_VARIANT_CONTEXT, async (variantContext) => { + this.#variantContext = variantContext; + this.#observeDisplayCulture(); + }); + } + + #observeDisplayCulture() { + this.observe( + this.#variantContext?.displayCulture, + (displayCulture) => { + if (!displayCulture) return; + if (this.#displayCulture.getValue() === displayCulture) return; + this.#displayCulture.setValue(displayCulture); + this.setFilter({ orderCulture: displayCulture }); + }, + 'umbDocumentCollectionDisplayCultureObserver', + ); + } + + public override async requestCollection() { + await this.observe(this.#displayCultureObservable)?.asPromise(); + await super.requestCollection(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-entity-actions-table-column-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-entity-actions-table-column-view.element.ts index 3a67955958..70f2852aae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-entity-actions-table-column-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-entity-actions-table-column-view.element.ts @@ -21,6 +21,7 @@ export class UmbDocumentEntityActionsTableColumnViewElement extends UmbLitElemen override render() { if (!this._value) return nothing; + // TODO: Missing name to parse on return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts index a3e88af6c9..11afa59568 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts @@ -6,6 +6,7 @@ import type { UmbDocumentTreeItemModel } from '../../tree/types.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbDocumentTypeEntityType } from '@umbraco-cms/backoffice/document-type'; +import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; interface UmbDocumentPickerInputContextOpenArgs { allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>; @@ -41,10 +42,14 @@ export class UmbDocumentPickerInputContext extends UmbPickerInputContext< }; } + const variantContext = await this.getContext(UMB_VARIANT_CONTEXT); + const culture = await variantContext?.getDisplayCulture(); + // pass allowedContentTypes to the search request args combinedPickerData.search!.queryParams = { allowedContentTypes: args?.allowedContentTypes, includeTrashed: args?.includeTrashed, + culture, ...pickerData?.search?.queryParams, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index f3af416fcf..6ce338324c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -1,3 +1,4 @@ +import type { UmbDocumentItemModel } from '../../item/types.js'; import { UmbDocumentPickerInputContext } from './input-document.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; @@ -7,7 +8,6 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/document-type'; -import type { UmbDocumentItemModel } from '../../item/types.js'; @customElement('umb-input-document') export class UmbInputDocumentElement extends UmbFormControlMixin( diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/repository/document-duplicate.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/repository/document-duplicate.server.data-source.ts index 3bf2bee93c..f2ce6d2621 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/repository/document-duplicate.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/repository/document-duplicate.server.data-source.ts @@ -39,6 +39,7 @@ export class UmbDuplicateDocumentServerDataSource { includeDescendants: args.includeDescendants, }, }), + { disableNotifications: true }, ); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/move-to/repository/document-move.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/move-to/repository/document-move.server.data-source.ts index 854e245ec7..ce765d743d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/move-to/repository/document-move.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/move-to/repository/document-move.server.data-source.ts @@ -39,6 +39,7 @@ export class UmbMoveDocumentServerDataSource implements UmbMoveDataSource { target: args.destination.unique ? { id: args.destination.unique } : null, }, }), + { disableNotifications: true }, ); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-data-resolver.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-data-resolver.ts index da25606180..0a072e6793 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-data-resolver.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-data-resolver.ts @@ -3,10 +3,8 @@ import type { UmbDocumentItemModel } from './types.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language'; -import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; -import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbBooleanState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { type UmbVariantContext, UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; type UmbDocumentItemDataResolverModel = Omit; @@ -17,51 +15,54 @@ type UmbDocumentItemDataResolverModel = Omit extends UmbControllerBase { - #defaultCulture?: string; - #appCulture?: string; - #propertyDataSetCulture?: UmbVariantId; - #data?: DataType | undefined; + #data = new UmbObjectState(undefined); - #init: Promise; - - #unique = new UmbStringState(undefined); - public readonly unique = this.#unique.asObservable(); + public readonly unique = this.#data.asObservablePart((x) => x?.unique); + public readonly icon = this.#data.asObservablePart((x) => x?.documentType.icon); + public readonly isTrashed = this.#data.asObservablePart((x) => x?.isTrashed); #name = new UmbStringState(undefined); public readonly name = this.#name.asObservable(); - #icon = new UmbStringState(undefined); - public readonly icon = this.#icon.asObservable(); - #state = new UmbStringState(undefined); public readonly state = this.#state.asObservable(); - #isTrashed = new UmbBooleanState(undefined); - public readonly isTrashed = this.#isTrashed.asObservable(); - #isDraft = new UmbBooleanState(undefined); public readonly isDraft = this.#isDraft.asObservable(); + #variantContext?: UmbVariantContext; + #fallbackCulture?: string | null; + #displayCulture?: string | null; + constructor(host: UmbControllerHost) { super(host); - this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => { - this.#propertyDataSetCulture = context?.getVariantId(); - this.#setVariantAwareValues(); + this.consumeContext(UMB_VARIANT_CONTEXT, (context) => { + this.#variantContext = context; + this.#observeVariantContext(); }); + } - // We do not depend on this context because we know is it only available in some cases - this.#init = this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (context) => { - this.observe(context?.appLanguageCulture, (culture) => { - this.#appCulture = culture; + #observeVariantContext() { + this.observe( + this.#variantContext?.displayCulture, + (displayCulture) => { + if (displayCulture === undefined) return; + this.#displayCulture = displayCulture; this.#setVariantAwareValues(); - }); + }, + 'umbObserveVariantId', + ); - this.observe(context?.appDefaultLanguage, (value) => { - this.#defaultCulture = value?.unique; + this.observe( + this.#variantContext?.fallbackCulture, + (fallbackCulture) => { + if (fallbackCulture === undefined) return; + this.#fallbackCulture = fallbackCulture; this.#setVariantAwareValues(); - }); - }).asPromise(); + }, + 'umbObserveFallbackCulture', + ); } /** @@ -70,7 +71,7 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#unique.getValue(); + return await this.observe(this.unique).asPromise(); } /** @@ -112,8 +99,7 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#name.getValue() || ''; + return (await this.observe(this.name).asPromise()) || ''; } /** @@ -122,8 +108,7 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#data?.documentType.icon; + return await this.observe(this.icon).asPromise(); } /** @@ -132,8 +117,8 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#getCurrentVariant()?.state; + const variant = await this.#getCurrentVariant(); + return variant?.state; } /** @@ -142,8 +127,7 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#isDraft.getValue() ?? false; + return (await this.observe(this.isDraft).asPromise()) ?? false; } /** @@ -152,52 +136,55 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#data?.isTrashed ?? false; + return (await this.observe(this.isTrashed).asPromise()) ?? false; } #setVariantAwareValues() { + if (!this.#variantContext) return; + if (!this.#displayCulture) return; + if (!this.#fallbackCulture) return; + if (!this.#data) return; this.#setName(); this.#setIsDraft(); this.#setState(); } - #setName() { - const variant = this.#getCurrentVariant(); + async #setName() { + const variant = await this.#getCurrentVariant(); if (variant) { this.#name.setValue(variant.name); return; } - const fallbackName = this.#findVariant(this.#defaultCulture)?.name; + + const fallbackName = this.#findVariant(this.#fallbackCulture!)?.name; this.#name.setValue(`(${fallbackName})`); } - #setIsDraft() { - const variant = this.#getCurrentVariant(); + async #setIsDraft() { + const variant = await this.#getCurrentVariant(); const isDraft = variant?.state === UmbDocumentVariantState.DRAFT || false; this.#isDraft.setValue(isDraft); } - #setState() { - const variant = this.#getCurrentVariant(); + async #setState() { + const variant = await this.#getCurrentVariant(); const state = variant?.state || UmbDocumentVariantState.NOT_CREATED; this.#state.setValue(state); } #findVariant(culture: string | undefined) { - return this.#data?.variants.find((x) => x.culture === culture); + return this.getData()?.variants.find((x) => x.culture === culture); } - #getCurrentVariant() { + async #getCurrentVariant() { if (this.#isInvariant()) { - return this.#data?.variants?.[0]; + return this.getData()?.variants?.[0]; } - const culture = this.#propertyDataSetCulture?.culture || this.#appCulture; - return this.#findVariant(culture); + return this.#findVariant(this.#displayCulture!); } #isInvariant() { - return this.#data?.variants?.[0]?.culture === null; + return this.getData()?.variants?.[0]?.culture === null; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-ref.element.ts index c75f764023..e37cc26fb1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-ref.element.ts @@ -6,6 +6,8 @@ import { customElement, html, ifDefined, nothing, property, state } from '@umbra import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; +import type { UUISelectableEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; @customElement('umb-document-item-ref') export class UmbDocumentItemRefElement extends UmbLitElement { @@ -19,12 +21,24 @@ export class UmbDocumentItemRefElement extends UmbLitElement { this.#item.setData(value); } - @property({ type: Boolean }) + @property({ type: Boolean, reflect: true }) readonly = false; @property({ type: Boolean }) standalone = false; + @property({ type: Boolean, attribute: 'select-only', reflect: true }) + selectOnly = false; + + @property({ type: Boolean, reflect: true }) + selectable = false; + + @property({ type: Boolean, reflect: true }) + selected = false; + + @property({ type: Boolean, reflect: true }) + disabled = false; + @state() private _unique = ''; @@ -68,6 +82,16 @@ export class UmbDocumentItemRefElement extends UmbLitElement { return `${this._editPath}/${path}`; } + #onSelected(event: UUISelectableEvent) { + event.stopPropagation(); + this.dispatchEvent(new UmbSelectedEvent(this._unique)); + } + + #onDeselected(event: UUISelectableEvent) { + event.stopPropagation(); + this.dispatchEvent(new UmbDeselectedEvent(this._unique)); + } + override render() { if (!this.item) return nothing; @@ -76,7 +100,13 @@ export class UmbDocumentItemRefElement extends UmbLitElement { name=${this._name} href=${ifDefined(this.#getHref())} ?readonly=${this.readonly} - ?standalone=${this.standalone}> + ?standalone=${this.standalone} + ?select-only=${this.selectOnly} + ?selectable=${this.selectable} + ?selected=${this.selected} + ?disabled=${this.disabled} + @selected=${this.#onSelected} + @deselected=${this.#onDeselected}> ${this.#renderIcon()}${this.#renderIsDraft()} ${this.#renderIsTrashed()} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.data-source.ts index a9e2dfe005..5c6453b35e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.data-source.ts @@ -4,6 +4,7 @@ import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/external import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A data source for Document items that fetches data from the server @@ -21,14 +22,24 @@ export class UmbDocumentItemServerDataSource extends UmbItemServerDataSourceBase */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => DocumentService.getItemDocument({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => DocumentService.getItemDocument({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: DocumentItemResponseModel): UmbDocumentItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts index 5b89bcfea9..aa2f39cceb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/save-modal/document-save-modal.element.ts @@ -57,10 +57,6 @@ export class UmbDocumentSaveModalElement extends UmbModalBaseElement< override render() { return html` -

- Choose which variants to be saved. -

- diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/picker/document-picker-search-result-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/picker/document-picker-search-result-item.element.ts index 52d0090b3e..d2bfc64c2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/picker/document-picker-search-result-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/picker/document-picker-search-result-item.element.ts @@ -1,24 +1,31 @@ import type { UmbDocumentSearchItemModel } from '../search/types.js'; +import type { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import { customElement, html, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbPickerSearchResultItemElementBase } from '@umbraco-cms/backoffice/picker'; @customElement('umb-document-picker-search-result-item') export class UmbDocumentPickerSearchResultItemElement extends UmbPickerSearchResultItemElementBase { + #onSelected(event: UmbSelectedEvent) { + if (event.unique !== this.item!.unique) return; + this.pickerContext?.selection.select(this.item!.unique); + } + + #onDeselected(event: UmbDeselectedEvent) { + if (event.unique !== this.item!.unique) return; + this.pickerContext?.selection.deselect(this.item!.unique); + } + override render() { if (!this.item) return nothing; - const item = this.item; return html` - this.pickerContext?.selection.deselect(item.unique)} - @selected=${() => this.pickerContext?.selection.select(item.unique)}> - + @deselected=${this.#onDeselected} + @selected=${this.#onSelected}> `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset.context.ts index 5b34ca61e1..66080404f6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset.context.ts @@ -1,9 +1,15 @@ -import type { UmbDocumentDetailModel, UmbDocumentVariantModel } from '../types.js'; +import type { UmbDocumentDetailModel, UmbDocumentVariantModel, UmbDocumentWorkspaceContext } from '../types.js'; import { UmbContentPropertyDatasetContext } from '@umbraco-cms/backoffice/content'; import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbDocumentPropertyDatasetContext extends UmbContentPropertyDatasetContext< UmbDocumentDetailModel, UmbDocumentTypeDetailModel, UmbDocumentVariantModel -> {} +> { + constructor(host: UmbControllerHost, dataOwner: UmbDocumentWorkspaceContext, variantId?: UmbVariantId) { + super(host, dataOwner, variantId); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts index 44a93f0d04..b59254f4c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/modal/document-publish-modal.element.ts @@ -101,26 +101,23 @@ export class UmbDocumentPublishModalElement extends UmbModalBaseElement< } override render() { - return html` + const headline = this.data?.headline ?? this.localize.term('content_publishModalTitle'); + + return html` +

+ +

+ ${when( !this._isInvariant, () => - html`

- Which variants would you like to publish? -

- `, + html` `, )} -

- - Publishing will make this page and all its published descendants visible on the site. - -

-
-

- ${when( - this._options.length > 1, - () => html` - Which languages would you like to schedule? - `, - () => html` - - Select the date and time to publish and/or unpublish the content item. - - `, - )} -

- ${this.#renderOptions()}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/unpublish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/unpublish.action.ts index 072941195b..667fb4dbeb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/unpublish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/unpublish.action.ts @@ -105,28 +105,11 @@ export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase } if (!error) { - // If the content is invariant, we need to show a different notification - const isInvariant = options.length === 1 && options[0].culture === null; - - if (isInvariant) { - notificationContext?.peek('positive', { - data: { - headline: localize.term('speechBubbles_editContentUnpublishedHeader'), - message: localize.term('speechBubbles_editContentUnpublishedText'), - }, - }); - } else { - const documentVariants = documentData.variants.filter((variant) => result.selection.includes(variant.culture!)); - notificationContext?.peek('positive', { - data: { - headline: localize.term('speechBubbles_editContentUnpublishedHeader'), - message: localize.term( - 'speechBubbles_editVariantUnpublishedText', - localize.list(documentVariants.map((v) => v.culture ?? v.name)), - ), - }, - }); - } + notificationContext?.peek('positive', { + data: { + message: localize.term('speechBubbles_editContentUnpublishedHeader'), + }, + }); const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); const event = new UmbRequestReloadStructureForEntityEvent({ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts index 7a1ef74b8f..d356a82072 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/modal/document-unpublish-modal.element.ts @@ -150,14 +150,12 @@ export class UmbDocumentUnpublishModalElement extends UmbModalBaseElement< override render() { return html` +

+ +

${when( !this._isInvariant, () => html` -

- - Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. - -

`, )} - -

- - Unpublishing will remove this page and all its descendants from the site. - -

- ${this._referencesConfig ? html` variantIds.some((id) => id.culture === v.culture)); this.#notificationContext?.peek('positive', { data: { - headline: this.#localize.term('speechBubbles_editContentPublishedHeader'), - message: this.#localize.term( - 'speechBubbles_editVariantPublishedText', - // TODO: use correct variant names instead of variant strings [MR] - this.#localize.list(variants.map((v) => UmbVariantId.Create(v).toString() ?? v.name)), - ), + message: this.#localize.term('speechBubbles_editContentPublishedHeader'), }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/constants.ts index 13e085c832..149d5cbed3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/constants.ts @@ -1 +1,2 @@ +export * from './global-search/constants.js'; export const UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Document'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search.server.data-source.ts index 891cac3642..9095364929 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document-search.server.data-source.ts @@ -35,9 +35,10 @@ export class UmbDocumentSearchServerDataSource this.#host, DocumentService.getItemDocumentSearch({ query: { - query: args.query, - parentId: args.searchFrom?.unique ?? undefined, allowedDocumentTypes: args.allowedContentTypes?.map((contentType) => contentType.unique), + culture: args.culture || undefined, + parentId: args.searchFrom?.unique ?? undefined, + query: args.query, trashed: args.includeTrashed, }, }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/constants.ts new file mode 100644 index 0000000000..b73b9ab78a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_DOCUMENT_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.Document'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/document-global-search.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/document-global-search.ts new file mode 100644 index 0000000000..c6997a0288 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/document-global-search.ts @@ -0,0 +1,28 @@ +import type { UmbDocumentSearchProvider } from '../document.search-provider.js'; +import { + UmbGlobalSearchBase, + type UmbGlobalSearchApi, + type UmbSearchRequestArgs, +} from '@umbraco-cms/backoffice/search'; +import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; + +export class UmbDocumentGlobalSearch + extends UmbGlobalSearchBase + implements UmbGlobalSearchApi +{ + override async search(args: UmbSearchRequestArgs) { + await this._init; + + if (!this._searchProvider) { + throw new Error('Search provider is not available'); + } + + // TODO: change this to consume so we don't emit context events for every search change [MR] + const variantContext = await this.getContext(UMB_VARIANT_CONTEXT); + const displayCulture = await variantContext?.getDisplayCulture(); + + return await this._searchProvider.search({ culture: displayCulture, query: args.query }); + } +} + +export { UmbDocumentGlobalSearch as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/manifests.ts new file mode 100644 index 0000000000..1b96d2cf32 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/global-search/manifests.ts @@ -0,0 +1,24 @@ +import { UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_DOCUMENT_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; +import { UMB_CONTENT_SECTION_ALIAS } from '@umbraco-cms/backoffice/content'; + +export const manifests: Array = [ + { + name: 'Document Global Search', + alias: UMB_DOCUMENT_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 800, + api: () => import('./document-global-search.js'), + meta: { + label: 'Documents', + searchProviderAlias: UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_CONTENT_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/manifests.ts index cfd1ce09e8..38ceced019 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/manifests.ts @@ -1,5 +1,6 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import { UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS } from './constants.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { @@ -19,4 +20,5 @@ export const manifests: Array = [ element: () => import('./document-search-result-item.element.js'), forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/types.ts index a5ffb39e58..0cd4b6984c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/types.ts @@ -9,4 +9,5 @@ export interface UmbDocumentSearchItemModel extends UmbDocumentItemModel { export interface UmbDocumentSearchRequestArgs extends UmbSearchRequestArgs { allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>; includeTrashed?: boolean; + culture?: string | null; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/document-urls-data-resolver.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/document-urls-data-resolver.ts new file mode 100644 index 0000000000..83ab17f7b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/document-urls-data-resolver.ts @@ -0,0 +1,94 @@ +import type { UmbDocumentUrlModel } from './repository/types.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; + +/** + * A controller for resolving data for document urls + * @exports + * @class UmbDocumentUrlsDataResolver + * @augments {UmbControllerBase} + */ +export class UmbDocumentUrlsDataResolver extends UmbControllerBase { + #appCulture?: string; + #propertyDataSetCulture?: UmbVariantId; + #data?: Array | undefined; + + #init: Promise; + + #urls = new UmbArrayState([], (url) => url.url); + /** + * The urls for the current culture + * @returns {ObservableArray} The urls for the current culture + * @memberof UmbDocumentUrlsDataResolver + */ + public readonly urls = this.#urls.asObservable(); + + constructor(host: UmbControllerHost) { + super(host); + + // TODO: listen for UMB_VARIANT_CONTEXT when available + this.#init = Promise.all([ + this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => { + this.#propertyDataSetCulture = context?.getVariantId(); + this.#setCultureAwareValues(); + }).asPromise(), + ]); + } + + /** + * Get the current data + * @returns {Array | undefined} The current data + * @memberof UmbDocumentUrlsDataResolver + */ + getData(): Array | undefined { + return this.#data; + } + + /** + * Set the current data + * @param {Array | undefined} data The current data + * @memberof UmbDocumentUrlsDataResolver + */ + setData(data: Array | undefined) { + this.#data = data; + + if (!this.#data) { + this.#urls.setValue([]); + return; + } + + this.#setCultureAwareValues(); + } + + /** + * Get the urls for the current culture + * @returns {(Promise | []>)} The urls for the current culture + * @memberof UmbDocumentUrlsDataResolver + */ + async getUrls(): Promise | []> { + await this.#init; + return this.#urls.getValue(); + } + + #setCultureAwareValues() { + this.#setUrls(); + } + + #setUrls() { + const data = this.#getDataForCurrentCulture(); + this.#urls.setValue(data ?? []); + } + + #getCurrentCulture(): string | undefined { + return this.#propertyDataSetCulture?.culture || this.#appCulture; + } + + #getDataForCurrentCulture(): Array | undefined { + const culture = this.#getCurrentCulture(); + // If there is no culture context (invariant data) we return all urls + return culture ? this.#data?.filter((x) => x.culture === culture) : this.#data; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/index.ts index 03eef61de7..0ca6b4e91b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/index.ts @@ -1,3 +1,4 @@ export { UmbDocumentUrlRepository, UMB_DOCUMENT_URL_REPOSITORY_ALIAS } from './repository/index.js'; export * from './constants.js'; +export * from './document-urls-data-resolver.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts index f0630d09a7..cab255c185 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts @@ -2,7 +2,17 @@ import { UmbDocumentUrlRepository } from '../repository/index.js'; import type { UmbDocumentVariantOptionModel } from '../../types.js'; import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../workspace/constants.js'; import type { UmbDocumentUrlModel } from '../repository/types.js'; -import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbDocumentUrlsDataResolver } from '../document-urls-data-resolver.js'; +import { + css, + customElement, + html, + ifDefined, + nothing, + repeat, + state, + when, +} from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action'; import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action'; @@ -10,11 +20,12 @@ import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import { debounce } from '@umbraco-cms/backoffice/utils'; -import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; interface UmbDocumentInfoViewLink { - culture: string; - url: string | undefined; + culture: string | null; + url: string | null | undefined; state: DocumentVariantStateModel | null | undefined; } @@ -37,13 +48,13 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement { @state() private _links: Array = []; - @state() - private _defaultCulture?: string; - #urls: Array = []; #documentWorkspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE; #eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; + #propertyDataSetVariantId?: UmbVariantId; + + #documentUrlsDataResolver? = new UmbDocumentUrlsDataResolver(this); constructor() { super(); @@ -88,20 +99,26 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement { }); }); - this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (instance) => { - this.observe(instance?.appDefaultLanguage, (value) => { - this._defaultCulture = value?.unique; - this.#setLinks(); - }); + this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => { + this.#propertyDataSetVariantId = context?.getVariantId(); + this.#setLinks(); + }); + + this.observe(this.#documentUrlsDataResolver?.urls, (urls) => { + this.#urls = urls ?? []; + this.#setLinks(); }); } #setLinks() { - const links: Array = this.#urls.map((u) => { - const culture = u.culture ?? this._defaultCulture ?? ''; - const url = u.url; + const links: Array = this.#urls.map((url) => { + const culture = url.culture; const state = this._variantOptions?.find((variantOption) => variantOption.culture === culture)?.variant?.state; - return { culture, url, state }; + return { + culture, + url: url.url, + state, + }; }); this._links = links; @@ -124,14 +141,12 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement { if (!this._unique) return; this._loading = true; - this.#urls = []; + this.#documentUrlsDataResolver?.setData([]); const { data } = await this.#documentUrlRepository.requestItems([this._unique]); if (data?.length) { - const item = data[0]; - this.#urls = item.urls; - this.#setLinks(); + this.#documentUrlsDataResolver?.setData(data[0].urls); } this._loading = false; @@ -207,7 +222,7 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement { } return html` - + ${this.#renderLinkCulture(link.culture)} ${link.url} @@ -218,9 +233,9 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement { } #renderNoLinks() { - return html` ${this._variantOptions?.map((variantOption) => - this.#renderEmptyLink(variantOption.culture, variantOption.variant?.state), - )}`; + return html` ${this._variantOptions + ?.filter((variantOption) => variantOption.culture === this.#propertyDataSetVariantId?.culture) + .map((variantOption) => this.#renderEmptyLink(variantOption.culture, variantOption.variant?.state))}`; } #renderEmptyLink(culture: string | null, state: DocumentVariantStateModel | null | undefined) { @@ -235,6 +250,8 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement { #renderLinkCulture(culture: string | null) { if (!culture) return nothing; if (this._links.length === 1) return nothing; + const allLinksHaveSameCulture = this._links?.every((link) => link.culture === culture); + if (allLinksHaveSameCulture) return nothing; return html`${culture}`; } @@ -249,10 +266,6 @@ export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement { static override styles = [ css` - uui-box { - --uui-box-default-padding: 0; - } - #loader-container { display: flex; justify-content: center; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.server.data-source.ts index bcf61c137d..8891d6b6c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.server.data-source.ts @@ -3,6 +3,7 @@ import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; import type { DocumentUrlInfoResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Document URLs @@ -19,11 +20,22 @@ export class UmbDocumentUrlServerDataSource extends UmbItemServerDataSourceBase< * @memberof UmbDocumentUrlServerDataSource */ constructor(host: UmbControllerHost) { - super(host, { getItems, mapper }); + super(host, { mapper }); + } + + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => DocumentService.getDocumentUrls({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; } } -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => DocumentService.getDocumentUrls({ query: { id: uniques } }); - const mapper = (item: DocumentUrlInfoResponseModel): UmbDocumentUrlsModel => ({ unique: item.id, urls: item.urlInfos }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/types.ts index 7291179f31..78089e5118 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/types.ts @@ -4,6 +4,6 @@ export interface UmbDocumentUrlsModel { } export interface UmbDocumentUrlModel { - culture?: string | null; + culture: string | null; url?: string; } 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 ea09e88092..009d2698a8 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 @@ -37,9 +37,9 @@ import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/documen import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; import { ensurePathEndsWithSlash, UmbDeprecation } from '@umbraco-cms/backoffice/utils'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; -import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; -import { UMB_LANGUAGE_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/language'; import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbVariantPropertyGuardRule } from '@umbraco-cms/backoffice/property'; type ContentModel = UmbDocumentDetailModel; type ContentTypeModel = UmbDocumentTypeDetailModel; @@ -93,6 +93,7 @@ export class UmbDocumentWorkspaceContext this.consumeContext(UMB_DOCUMENT_CONFIGURATION_CONTEXT, async (context) => { const config = await context?.getDocumentConfiguration(); const allowSegmentCreation = config?.allowNonExistingSegmentsCreation ?? false; + const allowEditInvariantFromNonDefault = config?.allowEditInvariantFromNonDefault ?? true; this._variantOptionsFilter = (variantOption) => { const isNotCreatedSegmentVariant = variantOption.segment && !variantOption.variant; @@ -104,12 +105,8 @@ export class UmbDocumentWorkspaceContext return true; }; - }); - this.consumeContext(UMB_DOCUMENT_CONFIGURATION_CONTEXT, async (context) => { - const documentConfiguration = (await context?.getDocumentConfiguration()) ?? undefined; - - if (documentConfiguration?.allowEditInvariantFromNonDefault !== true) { + if (allowEditInvariantFromNonDefault === false) { this.#preventEditInvariantFromNonDefault(); } }); @@ -228,46 +225,6 @@ export class UmbDocumentWorkspaceContext ]); } - #preventEditInvariantFromNonDefault() { - this.observe( - observeMultiple([this.structure.contentTypeProperties, this.languages]), - ([properties, languages]) => { - if (properties.length === 0) return; - if (languages.length === 0) return; - - const defaultLanguageUnique = languages.find((x) => x.isDefault)?.unique; - const ruleUnique = 'UMB_preventEditInvariantFromNonDefault'; - - const rule = { - unique: ruleUnique, - permitted: false, - message: 'Shared properties can only be edited in the default language', - variantId: UmbVariantId.CreateInvariant(), - }; - - /* The permission is false by default, and the onChange callback will not be triggered if the permission hasn't changed. - Therefore, we add the rule to the readOnlyGuard here. */ - this.propertyWriteGuard.addRule(rule); - - createExtensionApiByAlias(this, UMB_LANGUAGE_USER_PERMISSION_CONDITION_ALIAS, [ - { - config: { - allOf: [defaultLanguageUnique], - }, - onChange: (permitted: boolean) => { - if (permitted) { - this.propertyWriteGuard.removeRule(ruleUnique); - } else { - this.propertyWriteGuard.addRule(rule); - } - }, - }, - ]); - }, - 'observePreventEditInvariantFromNonDefault', - ); - } - override resetState(): void { super.resetState(); this.#isTrashedContext.setIsTrashed(false); @@ -352,7 +309,7 @@ export class UmbDocumentWorkspaceContext } public async saveAndPreview(): Promise { - return this.#handleSaveAndPreview(); + return await this.#handleSaveAndPreview(); } async #handleSaveAndPreview() { @@ -402,7 +359,7 @@ export class UmbDocumentWorkspaceContext solution: 'Use the Publish method on the UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT instead.', }).warn(); if (!this.#publishingContext) throw new Error('Publishing context is missing'); - this.#publishingContext.publish(); + await this.#publishingContext.publish(); } /** @@ -416,7 +373,7 @@ export class UmbDocumentWorkspaceContext solution: 'Use the SaveAndPublish method on the UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT instead.', }).warn(); if (!this.#publishingContext) throw new Error('Publishing context is missing'); - this.#publishingContext.saveAndPublish(); + await this.#publishingContext.saveAndPublish(); } /** @@ -430,7 +387,7 @@ export class UmbDocumentWorkspaceContext solution: 'Use the Schedule method on the UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT instead.', }).warn(); if (!this.#publishingContext) throw new Error('Publishing context is missing'); - this.#publishingContext.schedule(); + await this.#publishingContext.schedule(); } /** @@ -444,7 +401,7 @@ export class UmbDocumentWorkspaceContext solution: 'Use the Unpublish method on the UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT instead.', }).warn(); if (!this.#publishingContext) throw new Error('Publishing context is missing'); - this.#publishingContext.unpublish(); + await this.#publishingContext.unpublish(); } /** @@ -458,7 +415,7 @@ export class UmbDocumentWorkspaceContext solution: 'Use the PublishWithDescendants method on the UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT instead.', }).warn(); if (!this.#publishingContext) throw new Error('Publishing context is missing'); - this.#publishingContext.publishWithDescendants(); + await this.#publishingContext.publishWithDescendants(); } public createPropertyDatasetContext( @@ -479,6 +436,35 @@ export class UmbDocumentWorkspaceContext message, }); } + + #preventEditInvariantFromNonDefault() { + this.observe( + observeMultiple([this.structure.contentTypeProperties, this.variantOptions]), + ([properties, variantOptions]) => { + if (properties.length === 0) return; + if (variantOptions.length === 0) return; + + variantOptions.forEach((variantOption) => { + // Do not add a rule for the default language. It is always permitted to edit. + if (variantOption.language.isDefault) return; + + const datasetVariantId = UmbVariantId.CreateFromPartial(variantOption); + const invariantVariantId = UmbVariantId.CreateInvariant(); + const unique = `UMB_PREVENT_EDIT_INVARIANT_FROM_NON_DEFAULT_DATASET=${datasetVariantId.toString()}_PROPERTY_${invariantVariantId.toString()}`; + + const rule: UmbVariantPropertyGuardRule = { + unique, + message: 'Shared properties can only be edited in the default language', + variantId: invariantVariantId, + datasetVariantId, + permitted: false, + }; + + this.propertyWriteGuard.addRule(rule); + }); + }, + ); + } } export default UmbDocumentWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/extension-insights/collection/views/table/extension-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/extension-insights/collection/views/table/extension-table-collection-view.element.ts index 34017d758a..f00b4234c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/extension-insights/collection/views/table/extension-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/extension-insights/collection/views/table/extension-table-collection-view.element.ts @@ -84,6 +84,7 @@ export class UmbExtensionTableCollectionViewElement extends UmbLitElement { .value=${{ entityType: extension.entityType, unique: extension.unique, + name: extension.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts index c7a379909a..b347db3e81 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts @@ -100,6 +100,7 @@ export class UmbLanguageTableCollectionViewElement extends UmbLitElement { .value=${{ entityType: language.entityType, unique: language.unique, + name: language.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts index 684686c1cf..f3ae0cf99a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts @@ -8,6 +8,7 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import { UmbReadOnlyStateManager } from '@umbraco-cms/backoffice/utils'; import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UmbVariantContext } from '@umbraco-cms/backoffice/variant'; // TODO: Make a store for the App Languages. // TODO: Implement default language end-point, in progress at backend team, so we can avoid getting all languages. @@ -51,6 +52,8 @@ export class UmbAppLanguageContext extends UmbContextBase implements UmbApi { #readOnlyStateIdentifier = 'UMB_LANGUAGE_PERMISSION_'; #localStorageKey = 'umb:appLanguage'; + #variantContext = new UmbVariantContext(this); + constructor(host: UmbControllerHost) { super(host, UMB_APP_LANGUAGE_CONTEXT); @@ -96,6 +99,10 @@ export class UmbAppLanguageContext extends UmbContextBase implements UmbApi { // set the new language this.#appLanguage.update(language); + // Update the variant context with the new language + this.#variantContext.setCulture(language.unique); + this.#variantContext.setAppCulture(language.unique); + // store the new language in local storage localStorage.setItem(this.#localStorageKey, language?.unique); @@ -119,23 +126,20 @@ export class UmbAppLanguageContext extends UmbContextBase implements UmbApi { } #initAppLanguage() { - // get the selected language from local storage - const uniqueFromLocalStorage = localStorage.getItem(this.#localStorageKey); - - if (uniqueFromLocalStorage) { - const language = this.#findLanguage(uniqueFromLocalStorage); - if (language) { - this.setLanguage(language.unique); - return; - } - } - - const defaultLanguage = this.#languages.getValue().find((x) => x.isDefault); + const defaultLanguageUnique = this.#languages.getValue().find((x) => x.isDefault)?.unique; // TODO: do we always have a default language? // do we always get the default language on the first request, or could it be on page 2? // in that case do we then need an endpoint to get the default language? - if (!defaultLanguage?.unique) return; - this.setLanguage(defaultLanguage.unique); + if (!defaultLanguageUnique) return; + + this.#variantContext.setFallbackCulture(defaultLanguageUnique); + + // get the selected language from local storage + const uniqueFromLocalStorage = localStorage.getItem(this.#localStorageKey); + const languageFromLocalStorage = this.#findLanguage(uniqueFromLocalStorage || ''); + const languageUniqueToSet = languageFromLocalStorage ? languageFromLocalStorage.unique : defaultLanguageUnique; + + this.setLanguage(languageUniqueToSet); } #findLanguage(unique: string) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/menu/language-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/menu/language-menu-structure.context.ts index d41cb2dd27..8aa131c2e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/menu/language-menu-structure.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/menu/language-menu-structure.context.ts @@ -4,7 +4,7 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbStructureItemModel } from '@umbraco-cms/backoffice/menu'; +import { UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT, type UmbStructureItemModel } from '@umbraco-cms/backoffice/menu'; export class UmbLanguageNavigationStructureWorkspaceContext extends UmbContextBase { // TODO: figure out the correct type where we have "data" available @@ -14,8 +14,9 @@ export class UmbLanguageNavigationStructureWorkspaceContext extends UmbContextBa public readonly structure = this.#structure.asObservable(); constructor(host: UmbControllerHost) { - // TODO: set up context token - super(host, 'UmbMenuStructureWorkspaceContext'); + super(host, UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT); + // 'UmbMenuStructureWorkspaceContext' is Obsolete, will be removed in v.18 + this.provideContext('UmbMenuStructureWorkspaceContext', this); this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => { this.#workspaceContext = instance; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/repository/item/language-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/language/repository/item/language-item.server.data-source.ts index 3e9b7e5fe9..b38c275278 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/repository/item/language-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/repository/item/language-item.server.data-source.ts @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { LanguageItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { LanguageService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Language items @@ -21,14 +22,24 @@ export class UmbLanguageItemServerDataSource extends UmbItemServerDataSourceBase */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => LanguageService.getItemLanguage({ query: { isoCode: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => LanguageService.getItemLanguage({ query: { isoCode: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: LanguageItemResponseModel): UmbLanguageItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/constants.ts index 5bc61b44e3..c80e3ac5e4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/constants.ts @@ -5,6 +5,7 @@ export * from './property-type/constants.js'; export * from './repository/constants.js'; export * from './tree/constants.js'; export * from './workspace/constants.js'; +export * from './search/constants.js'; export { UMB_MEDIA_TYPE_ENTITY_TYPE, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.data-source.ts index 8897295e51..402cb410a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.data-source.ts @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { MediaTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MediaTypeService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A data source for Media Type items that fetches data from the server @@ -21,14 +22,24 @@ export class UmbMediaTypeItemServerDataSource extends UmbItemServerDataSourceBas */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => MediaTypeService.getItemMediaType({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => MediaTypeService.getItemMediaType({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: MediaTypeItemResponseModel): UmbMediaTypeItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/constants.ts new file mode 100644 index 0000000000..8be35fc954 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/constants.ts @@ -0,0 +1,2 @@ +export * from './global-search/constants.js'; +export const UMB_MEDIA_TYPE_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.MediaType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/global-search/constants.ts new file mode 100644 index 0000000000..00075ece97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_MEDIA_TYPE_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.MediaType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/global-search/manifests.ts new file mode 100644 index 0000000000..f2c89ec4fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/global-search/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_MEDIA_TYPE_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_MEDIA_TYPE_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; +import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; + +export const manifests: Array = [ + { + name: 'Media Type Global Search', + alias: UMB_MEDIA_TYPE_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 500, + meta: { + label: 'Media Types', + searchProviderAlias: UMB_MEDIA_TYPE_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_SETTINGS_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/manifests.ts index 4bf8dcd6d6..655fb27057 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/search/manifests.ts @@ -1,9 +1,11 @@ import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../entity.js'; +import { UMB_MEDIA_TYPE_SEARCH_PROVIDER_ALIAS } from './constants.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { name: 'Media Type Search Provider', - alias: 'Umb.SearchProvider.MediaType', + alias: UMB_MEDIA_TYPE_SEARCH_PROVIDER_ALIAS, type: 'searchProvider', api: () => import('./media-type.search-provider.js'), weight: 500, @@ -17,4 +19,5 @@ export const manifests: Array = [ type: 'searchResultItem', forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/tree-item-children/collection/views/media-type-tree-item-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/tree-item-children/collection/views/media-type-tree-item-table-collection-view.element.ts index 8ae49c7691..cce4f835c8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/tree-item-children/collection/views/media-type-tree-item-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/tree-item-children/collection/views/media-type-tree-item-table-collection-view.element.ts @@ -94,6 +94,7 @@ export class UmbMediaTypeTreeItemTableCollectionViewElement extends UmbLitElemen .value=${{ entityType: item.entityType, unique: item.unique, + name: item.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts index 75cc8be0a0..d73710897a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts @@ -22,6 +22,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { super(); this.consumeContext(UMB_MEDIA_COLLECTION_CONTEXT, (context) => { + // TODO: stop consuming the context both in the default element and here. Instead make the default able to inform when the context is consumed. Or come up with a better system for the controllers to talk together. [NL] this.#collectionContext = context; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts index d7b59b22ed..c027c36009 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts @@ -144,6 +144,7 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { .value=${{ entityType: item.entityType, unique: item.unique, + name: item.name, }}>`, }; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts index 338cd0fe35..ef2ebb8d85 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts @@ -7,6 +7,7 @@ import type { UmbMediaTreeItemModel } from '../../tree/types.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbMediaTypeEntityType } from '@umbraco-cms/backoffice/media-type'; +import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; interface UmbMediaPickerInputContextOpenArgs { allowedContentTypes?: Array<{ unique: string; entityType: UmbMediaTypeEntityType }>; @@ -39,10 +40,14 @@ export class UmbMediaPickerInputContext extends UmbPickerInputContext< }; } + const variantContext = await this.getContext(UMB_VARIANT_CONTEXT); + const culture = await variantContext?.getDisplayCulture(); + // pass allowedContentTypes to the search request args combinedPickerData.search!.queryParams = { allowedContentTypes: args?.allowedContentTypes, includeTrashed: args?.includeTrashed, + culture, ...pickerData?.search?.queryParams, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts index 9e21ba81da..f4b9948687 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts @@ -3,6 +3,7 @@ export * from './entity-actions/constants.js'; export * from './entity-bulk-actions/constants.js'; export * from './menu/constants.js'; export * from './modals/constants.js'; +export * from './paths.js'; export * from './recycle-bin/constants.js'; export * from './reference/constants.js'; export * from './repository/constants.js'; @@ -10,7 +11,6 @@ export * from './search/constants.js'; export * from './tree/constants.js'; export * from './url/constants.js'; export * from './workspace/constants.js'; -export * from './paths.js'; export { UMB_MEDIA_VARIANT_CONTEXT } from './property-dataset-context/media-property-dataset-context.token.js'; export { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index a2823f1e24..c9510ae72e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -25,6 +25,7 @@ import { UMB_CONTENT_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content'; import type { UUIInputEvent, UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; import { isUmbracoFolder } from '@umbraco-cms/backoffice/media-type'; import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; import '@umbraco-cms/backoffice/imaging'; @@ -76,6 +77,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement(); + #contextCulture?: string | null; constructor() { super(); @@ -85,6 +87,12 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement { + this.observe(context?.culture, (culture) => { + this.#contextCulture = culture; + }); + }); } override async connectedCallback(): Promise { @@ -203,6 +211,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement { - #host: UmbControllerHost; /** * Creates an instance of UmbMediaItemServerDataSource. * @param {UmbControllerHost} host - The controller host for this controller to be appended to @@ -23,10 +23,8 @@ export class UmbMediaItemServerDataSource extends UmbItemServerDataSourceBase< */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); - this.#host = host; } /** @@ -38,17 +36,25 @@ export class UmbMediaItemServerDataSource extends UmbItemServerDataSourceBase< * ``` */ async search({ query, skip, take }: { query: string; skip: number; take: number }) { - const { data, error } = await tryExecute( - this.#host, - MediaService.getItemMediaSearch({ query: { query, skip, take } }), - ); + const { data, error } = await tryExecute(this, MediaService.getItemMediaSearch({ query: { query, skip, take } })); const mapped = data?.items.map((item) => mapper(item)); return { data: mapped, error }; } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => MediaService.getItemMedia({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => MediaService.getItemMedia({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: MediaItemResponseModel): UmbMediaItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/constants.ts index 8a888cf4ac..91822eed55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/constants.ts @@ -1 +1,2 @@ +export * from './global-search/constants.js'; export const UMB_MEDIA_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Media'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/constants.ts new file mode 100644 index 0000000000..82afcc341f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_MEDIA_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.Media'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/manifests.ts new file mode 100644 index 0000000000..583a7ac849 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/manifests.ts @@ -0,0 +1,24 @@ +import { UMB_MEDIA_SECTION_ALIAS } from '../../../media-section/constants.js'; +import { UMB_MEDIA_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_MEDIA_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; + +export const manifests: Array = [ + { + name: 'Media Global Search', + alias: UMB_MEDIA_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 700, + api: () => import('./media-global-search.js'), + meta: { + label: 'Media', + searchProviderAlias: UMB_MEDIA_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_MEDIA_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/media-global-search.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/media-global-search.ts new file mode 100644 index 0000000000..47891bb0d9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/global-search/media-global-search.ts @@ -0,0 +1,25 @@ +import type { UmbMediaSearchProvider } from '../media.search-provider.js'; +import { + UmbGlobalSearchBase, + type UmbGlobalSearchApi, + type UmbSearchRequestArgs, +} from '@umbraco-cms/backoffice/search'; +import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; + +export class UmbMediaGlobalSearch extends UmbGlobalSearchBase implements UmbGlobalSearchApi { + override async search(args: UmbSearchRequestArgs) { + await this._init; + + if (!this._searchProvider) { + throw new Error('Search provider is not available'); + } + + // TODO: change this to consume so we don't emit context events for every search change [MR] + const variantContext = await this.getContext(UMB_VARIANT_CONTEXT); + const displayCulture = await variantContext?.getDisplayCulture(); + + return await this._searchProvider.search({ culture: displayCulture, query: args.query }); + } +} + +export { UmbMediaGlobalSearch as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/manifests.ts index 62d6003c17..5442f9672b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/manifests.ts @@ -1,5 +1,6 @@ import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; import { UMB_MEDIA_SEARCH_PROVIDER_ALIAS } from './constants.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { @@ -19,4 +20,5 @@ export const manifests: Array = [ element: () => import('./media-search-result-item.element.js'), forEntityTypes: [UMB_MEDIA_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media-search.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media-search.server.data-source.ts index bbb0db506d..ff0fb28bd3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media-search.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media-search.server.data-source.ts @@ -35,9 +35,10 @@ export class UmbMediaSearchServerDataSource this.#host, MediaService.getItemMediaSearch({ query: { - query: args.query, - parentId: args.searchFrom?.unique || undefined, allowedMediaTypes: args.allowedContentTypes?.map((mediaReference) => mediaReference.unique), + culture: args.culture || undefined, + parentId: args.searchFrom?.unique || undefined, + query: args.query, trashed: args.includeTrashed, }, }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/types.ts index cda2b99b74..4ab35c34f1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/types.ts @@ -9,4 +9,5 @@ export interface UmbMediaSearchItemModel extends UmbMediaItemModel { export interface UmbMediaSearchRequestArgs extends UmbSearchRequestArgs { allowedContentTypes?: Array<{ unique: string; entityType: UmbMediaTypeEntityType }>; includeTrashed?: boolean; + culture?: string | null; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.server.data-source.ts index d52e8952c6..24571d50d3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.server.data-source.ts @@ -1,5 +1,6 @@ import type { UmbMediaUrlModel } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; import { MediaService, type MediaUrlInfoResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; @@ -19,14 +20,24 @@ export class UmbMediaUrlServerDataSource extends UmbItemServerDataSourceBase< */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => MediaService.getMediaUrls({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => MediaService.getMediaUrls({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: MediaUrlInfoResponseModel): UmbMediaUrlModel => { const url = item.urlInfos.length ? item.urlInfos[0].url : undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts index 95e1e3399f..2ad0df8c20 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts @@ -63,6 +63,7 @@ export class UmbMemberGroupTableCollectionViewElement extends UmbLitElement { .value=${{ entityType: memberGroup.entityType, unique: memberGroup.unique, + name: memberGroup.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/member-group-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/member-group-item.server.data-source.ts index 3bdf68a8ee..6840673866 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/member-group-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/member-group-item.server.data-source.ts @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { MemberGroupItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MemberGroupService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Member Group items @@ -21,14 +22,24 @@ export class UmbMemberGroupItemServerDataSource extends UmbItemServerDataSourceB */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => MemberGroupService.getItemMemberGroup({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => MemberGroupService.getItemMemberGroup({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: MemberGroupItemResponseModel): UmbMemberGroupItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/constants.ts index 6a7f64eed0..5f144502c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/constants.ts @@ -3,6 +3,7 @@ export * from './member-type-root/constants.js'; export * from './paths.js'; export * from './property-type/constants.js'; export * from './repository/constants.js'; +export * from './search/constants.js'; export * from './tree/constants.js'; export * from './workspace/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/item/member-type-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/item/member-type-item.server.data-source.ts index c83d7443b9..68444ad9ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/item/member-type-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/item/member-type-item.server.data-source.ts @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { MemberTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MemberTypeService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Member Type items @@ -21,14 +22,24 @@ export class UmbMemberTypeItemServerDataSource extends UmbItemServerDataSourceBa */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => MemberTypeService.getItemMemberType({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => MemberTypeService.getItemMemberType({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: MemberTypeItemResponseModel): UmbMemberTypeItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/constants.ts new file mode 100644 index 0000000000..3e72d59c99 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/constants.ts @@ -0,0 +1,2 @@ +export * from './global-search/constants.js'; +export const UMB_MEMBER_TYPE_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.MemberType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/global-search/constants.ts new file mode 100644 index 0000000000..4c2ce3c6dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_MEMBER_TYPE_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.MemberType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/global-search/manifests.ts new file mode 100644 index 0000000000..b2b58efc73 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/global-search/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_MEMBER_TYPE_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_MEMBER_TYPE_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; +import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; + +export const manifests: Array = [ + { + name: 'Member Type Global Search', + alias: UMB_MEMBER_TYPE_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 200, + meta: { + label: 'Member Types', + searchProviderAlias: UMB_MEMBER_TYPE_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_SETTINGS_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/manifests.ts index 4062f3a23b..d7a5302583 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/search/manifests.ts @@ -1,9 +1,11 @@ import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '../entity.js'; +import { UMB_MEMBER_TYPE_SEARCH_PROVIDER_ALIAS } from './constants.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { name: 'Member Type Search Provider', - alias: 'Umb.SearchProvider.MemberType', + alias: UMB_MEMBER_TYPE_SEARCH_PROVIDER_ALIAS, type: 'searchProvider', api: () => import('./member-type.search-provider.js'), weight: 200, @@ -17,4 +19,5 @@ export const manifests: Array = [ type: 'searchResultItem', forEntityTypes: [UMB_MEMBER_TYPE_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts index 02f8cb5643..b228d62127 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts @@ -108,6 +108,7 @@ export class UmbMemberTableCollectionViewElement extends UmbLitElement { .value=${{ entityType: member.entityType, unique: member.unique, + name: member.variants[0].name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/item/repository/member-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/item/repository/member-item.server.data-source.ts index 2853948b74..adc992d919 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/item/repository/member-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/item/repository/member-item.server.data-source.ts @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { MemberItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MemberService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Member items @@ -21,14 +22,24 @@ export class UmbMemberItemServerDataSource extends UmbItemServerDataSourceBase< */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => MemberService.getItemMember({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => MemberService.getItemMember({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: MemberItemResponseModel): UmbMemberItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/constants.ts index 8cca6e6a79..91ead50980 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/constants.ts @@ -1 +1,2 @@ +export * from './global-search/constants.js'; export const UMB_MEMBER_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Member'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/global-search/constants.ts new file mode 100644 index 0000000000..845b0f09ff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_MEMBER_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.Member'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/global-search/manifests.ts new file mode 100644 index 0000000000..6fb2485c10 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/global-search/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_MEMBER_MANAGEMENT_SECTION_ALIAS } from '../../../section/constants.js'; +import { UMB_MEMBER_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_MEMBER_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; + +export const manifests: Array = [ + { + name: 'Member Global Search', + alias: UMB_MEMBER_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 300, + meta: { + label: 'Members', + searchProviderAlias: UMB_MEMBER_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_MEMBER_MANAGEMENT_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/manifests.ts index c285ebf573..2db3199ca6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/manifests.ts @@ -1,5 +1,6 @@ import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js'; import { UMB_MEMBER_SEARCH_PROVIDER_ALIAS } from './constants.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { @@ -18,4 +19,5 @@ export const manifests: Array = [ type: 'searchResultItem', forEntityTypes: [UMB_MEMBER_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index 69171ad191..f7e23b6231 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -21,6 +21,8 @@ import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; +import { UmbDocumentUrlRepository, UmbDocumentUrlsDataResolver } from '@umbraco-cms/backoffice/document'; +import { UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media'; /** * @element umb-input-multi-url @@ -129,6 +131,7 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, this.#urls = [...data]; // Unfreeze data coming from State, so we can manipulate it. super.value = this.#urls.map((x) => x.url).join(','); this.#sorter.setModel(this.#urls); + this.#populateLinksUrl(); } get urls(): Array { return this.#urls; @@ -160,6 +163,9 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, @state() private _modalRoute?: UmbModalRouteBuilder; + @state() + _resolvedLinkUrls: Array<{ unique: string; url: string }> = []; + #linkPickerModal; constructor() { @@ -229,6 +235,49 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, }); } + #populateLinksUrl() { + // Documents and media have URLs saved in the local link format. Display the actual URL to align with what + // the user sees when they selected it initially. + this.#urls.forEach(async (link) => { + if (!link.unique) return; + + let url: string | undefined = undefined; + switch (link.type) { + case 'document': { + url = await this.#getUrlForDocument(link.unique); + break; + } + case 'media': { + url = await this.#getUrlForMedia(link.unique); + break; + } + default: + break; + } + + if (url) { + const resolvedUrl = { unique: link.unique, url }; + this._resolvedLinkUrls = [...this._resolvedLinkUrls, resolvedUrl]; + } + }); + } + + async #getUrlForDocument(unique: string) { + const documentUrlRepository = new UmbDocumentUrlRepository(this); + const { data: documentUrlData } = await documentUrlRepository.requestItems([unique]); + const urlsItem = documentUrlData?.[0]; + const dataResolver = new UmbDocumentUrlsDataResolver(this); + dataResolver.setData(urlsItem?.urls); + const resolvedUrls = await dataResolver.getUrls(); + return resolvedUrls?.[0]?.url ?? ''; + } + + async #getUrlForMedia(unique: string) { + const mediaUrlRepository = new UmbMediaUrlRepository(this); + const { data: mediaUrlData } = await mediaUrlRepository.requestItems([unique]); + return mediaUrlData?.[0].url ?? ''; + } + async #requestRemoveItem(index: number) { const item = this.#urls[index]; if (!item) throw new Error('Could not find item at index: ' + index); @@ -307,12 +356,13 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, #renderItem(link: UmbLinkPickerLink, index: number) { const unique = this.#getUnique(link); const href = this.readonly ? undefined : (this._modalRoute?.({ index }) ?? undefined); + const resolvedUrl = this._resolvedLinkUrls.find((url) => url.unique === link.unique)?.url ?? ''; return html` ${when( 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 31a1997820..f8cdb79671 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 @@ -6,13 +6,83 @@ import type { } from './link-picker-modal.token.js'; import { css, customElement, html, nothing, query, state, when } from '@umbraco-cms/backoffice/external/lit'; import { isUmbracoFolder, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; -import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { + UMB_VALIDATION_CONTEXT, + umbBindToValidation, + UmbObserveValidationStateController, + UmbValidationContext, + type UmbValidator, +} from '@umbraco-cms/backoffice/validation'; import { umbConfirmModal, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UmbDocumentDetailRepository, UmbDocumentUrlRepository } from '@umbraco-cms/backoffice/document'; -import { UmbMediaDetailRepository, UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media'; +import { + UmbDocumentItemDataResolver, + UmbDocumentItemRepository, + UmbDocumentUrlRepository, + UmbDocumentUrlsDataResolver, +} from '@umbraco-cms/backoffice/document'; +import { UmbMediaItemRepository, UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media'; import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; -import type { UUIBooleanInputEvent, UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIBooleanInputEvent, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; + +class UmbLinkPickerValueValidator extends UmbControllerBase implements UmbValidator { + #context?: typeof UMB_VALIDATION_CONTEXT.TYPE; + + #isValid = true; + get isValid(): boolean { + return this.#isValid; + } + + #value: unknown; + + #unique = 'UmbLinkPickerValueValidator'; + + setValue(value: unknown) { + this.#value = value; + this.validate(); + } + + getValue(): unknown { + return this.#value; + } + + // The path to the data that this validator is validating. + readonly #dataPath: string; + + constructor(host: UmbControllerHost, dataPath: string) { + super(host); + this.#dataPath = dataPath; + this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => { + if (this.#context) { + this.#context.removeValidator(this); + } + this.#context = context; + context?.addValidator(this); + }); + } + + async validate(): Promise { + this.#isValid = !!this.getValue(); + + if (this.#isValid) { + this.#context?.messages.removeMessageByKey(this.#unique); + } else { + this.#context?.messages.addMessage( + 'client', + this.#dataPath, + '#linkPicker_modalAnchorValidationMessage', + this.#unique, + ); + } + } + + reset(): void {} + + focusFirstInvalidElement(): void {} +} type UmbInputPickerEvent = CustomEvent & { target: { value?: string } }; @@ -31,14 +101,22 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement { + this._missingLinkUrl = invalid; + }); + } override connectedCallback() { super.connectedCallback(); @@ -56,14 +134,13 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement this.localize.term('linkPicker_modalAnchorValidationMessage'), - () => !this.value.link.url && !this.value.link.queryString, - ); + const validator = new UmbLinkPickerValueValidator(this, '$.type'); + + this.observe(this.modalContext?.value, (value) => { + validator.setValue(value?.link.type); + }); } async #getMediaTypes() { @@ -74,6 +151,30 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement x.unique).filter((x) => x && !isUmbracoFolder(x)) as Array) ?? []; } + async populateLinkUrl() { + // Documents and media have URLs saved in the local link format. Display the actual URL to align with what + // the user sees when they selected it initially. + if (!this.value.link?.unique) return; + + let url: string | undefined = undefined; + switch (this.value.link.type) { + case 'document': { + url = await this.#getUrlForDocument(this.value.link.unique); + break; + } + case 'media': { + url = await this.#getUrlForMedia(this.value.link.unique); + break; + } + default: + break; + } + + if (url) { + this.#partialUpdateLink({ url }); + } + } + #partialUpdateLink(linkObject: Partial) { this.modalContext?.updateValue({ link: { ...this.value.link, ...linkObject } }); } @@ -131,39 +232,40 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement ${this.#renderLinkType()} ${this.#renderLinkAnchorInput()} ${this.#renderLinkTitleInput()} @@ -219,8 +341,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement
@@ -232,7 +353,8 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement + mandatory + ?invalid=${this._missingLinkUrl}>
${this.#renderLinkTypeSelection()} ${this.#renderDocumentPicker()} ${this.#renderMediaPicker()} ${this.#renderLinkUrlInput()} ${this.#renderLinkUrlInputReadOnly()} @@ -295,9 +417,10 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement + required + @input=${this.#onLinkUrlInput} + ${umbBindToValidation(this)} + ${umbFocus()}> ${when( !this.value.link.unique, () => html` @@ -330,12 +453,10 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement + @change=${this.#onLinkAnchorInput}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view-item.element.ts index 6b6f3a8a2c..6402ba7be6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view-item.element.ts @@ -131,7 +131,7 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement { .state=${this._migrationButtonState} color="warning" look="primary" - label=${this.localize.term('packageMigrationsRun')}>` + label=${this.localize.term('packager_packageMigrationsRun')}>` : nothing}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/workspace-info-app/entity-references-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/workspace-info-app/entity-references-workspace-view-info.element.ts index 2cb60ee4a4..ac64050dda 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/workspace-info-app/entity-references-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/workspace-info-app/entity-references-workspace-view-info.element.ts @@ -106,7 +106,7 @@ export class UmbEntityReferencesWorkspaceInfoAppElement extends UmbLitElement { if (!this._items?.length) return nothing; return html` - ${this.#renderItems()} ${this.#renderReferencePagination()} +
${this.#renderItems()} ${this.#renderReferencePagination()}
`; } @@ -150,6 +150,11 @@ export class UmbEntityReferencesWorkspaceInfoAppElement extends UmbLitElement { display: contents; } + #content { + display: block; + padding: var(--uui-size-space-3) var(--uui-size-space-4); + } + .pagination-container { display: flex; justify-content: center; diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search-base.ts b/src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search-base.ts new file mode 100644 index 0000000000..9381e86744 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search-base.ts @@ -0,0 +1,69 @@ +import type { + ManifestGlobalSearch, + ManifestSearchProvider, + UmbGlobalSearchApi, + UmbSearchProvider, + UmbSearchRequestArgs, +} from '../types.js'; +import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +export abstract class UmbGlobalSearchBase + extends UmbControllerBase + implements UmbGlobalSearchApi +{ + protected _manifest?: ManifestGlobalSearch; + protected _searchProvider?: SearchProviderType; + #initResolver?: () => void; + #initialized = false; + + protected _init = new Promise((resolve) => { + if (this.#initialized) { + resolve(); + } else { + this.#initResolver = resolve; + } + }); + + #checkIfInitialized() { + if (this._searchProvider) { + this.#initialized = true; + this.#initResolver?.(); + } + } + + public set manifest(manifest: ManifestGlobalSearch | undefined) { + if (this._manifest === manifest) return; + this._manifest = manifest; + this.#observeSearchProvider(this._manifest?.meta.searchProviderAlias); + } + public get manifest() { + return this._manifest; + } + + #observeSearchProvider(alias?: string) { + if (!alias) throw new Error('Search provider alias is required'); + + new UmbExtensionApiInitializer( + this, + umbExtensionsRegistry, + alias, + [this], + (permitted, ctrl) => { + this._searchProvider = permitted ? ctrl.api : undefined; + this.#checkIfInitialized(); + }, + ); + } + + async search(args: UmbSearchRequestArgs) { + await this._init; + + if (!this._searchProvider) { + throw new Error('Search provider is not available'); + } + + return await this._searchProvider.search({ query: args.query }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search.extension.ts new file mode 100644 index 0000000000..5a21f0c108 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search.extension.ts @@ -0,0 +1,20 @@ +import type { UmbGlobalSearchApi } from './types.js'; +import type { ManifestWithDynamicConditions, ManifestApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestGlobalSearch + extends ManifestApi, + ManifestWithDynamicConditions { + type: 'globalSearch'; + meta: MetaGlobalSearch; +} + +export interface MetaGlobalSearch { + label: string; + searchProviderAlias: string; +} + +declare global { + interface UmbExtensionManifestMap { + umbGlobalSearch: ManifestGlobalSearch; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/global-search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/search/global-search/index.ts new file mode 100644 index 0000000000..c57eece95d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/search/global-search/index.ts @@ -0,0 +1 @@ +export * from './global-search-base.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/global-search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/search/global-search/types.ts new file mode 100644 index 0000000000..6655163258 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/search/global-search/types.ts @@ -0,0 +1,9 @@ +import type { UmbSearchProvider, UmbSearchRequestArgs, UmbSearchResultItemModel } from '../types.js'; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbGlobalSearchApi< + SearchResultItemType extends UmbSearchResultItemModel = UmbSearchResultItemModel, + SearchRequestArgsType extends UmbSearchRequestArgs = UmbSearchRequestArgs, +> extends UmbSearchProvider {} + +export type * from './global-search.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/search/index.ts index 0273dec301..c2ec4cc724 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/search/index.ts @@ -1,2 +1,3 @@ export * from './constants.js'; +export * from './global-search/index.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/search-modal/search-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/search/search-modal/search-modal.element.ts index 30377229d6..5ed8d2c6ce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/search-modal/search-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/search/search-modal/search-modal.element.ts @@ -1,5 +1,6 @@ import type { UmbSearchProvider, UmbSearchResultItemModel } from '../types.js'; import type { ManifestSearchResultItem } from '../extensions/types.js'; +import type { UmbGlobalSearchApi } from '../global-search/types.js'; import { css, html, @@ -12,16 +13,16 @@ import { when, } from '@umbraco-cms/backoffice/external/lit'; import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { createExtensionApiByAlias, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; import '../search-result/search-result-item.element.js'; -type SearchProvider = { +type GlobalSearchers = { name: string; - api: UmbSearchProvider; + api?: UmbSearchProvider; alias: string; }; @@ -42,10 +43,10 @@ export class UmbSearchModalElement extends UmbLitElement { private _searchResults: Array = []; @state() - private _searchProviders: Array = []; + private _globalSearchers: Array = []; @state() - _currentProvider?: SearchProvider; + _currentGlobalSearcher?: GlobalSearchers; @state() _loading: boolean = false; @@ -58,7 +59,7 @@ export class UmbSearchModalElement extends UmbLitElement { constructor() { super(); - this.#observeProviders(); + this.#observeGlobalSearchers(); } override connectedCallback() { @@ -86,25 +87,42 @@ export class UmbSearchModalElement extends UmbLitElement { this.modalContext?.reject(); }; - #observeProviders() { - new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'searchProvider', null, async (providers) => { - const searchProviders: Array = []; + #observeGlobalSearchers() { + new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'globalSearch', null, async (controllers) => { + const globalSearch: Array = []; - for (const provider of providers) { - const api = await createExtensionApi>(this, provider.manifest); - if (api) { - searchProviders.push({ - name: provider.manifest.meta?.label || provider.manifest.name, - api, - alias: provider.alias, - }); + for (const controller of controllers) { + let searchApi = undefined; + + if (controller.manifest.api) { + searchApi = await createExtensionApi(this, controller.manifest); + + if (searchApi) { + // TODO: we need to investigate if it makes sense to have a function that does this when creating a new extension api? [MR] + (searchApi as any).manifest = controller.manifest; + } + } else { + searchApi = await createExtensionApiByAlias>( + this, + controller.manifest.meta?.searchProviderAlias, + ); + } + + if (searchApi) { + const searcher: GlobalSearchers = { + name: controller.manifest.meta?.label || controller.manifest.name, + api: searchApi, + alias: controller.alias, + }; + + globalSearch.push(searcher); } } - this._searchProviders = searchProviders; + this._globalSearchers = globalSearch; - if (this._searchProviders.length > 0) { - this._currentProvider = this._searchProviders[0]; + if (this._globalSearchers.length > 0) { + this._currentGlobalSearcher = this._globalSearchers[0]; } }); } @@ -141,10 +159,10 @@ export class UmbSearchModalElement extends UmbLitElement { } } - #setCurrentProvider(searchProvider: SearchProvider) { - if (this._currentProvider === searchProvider) return; + #setCurrentSearcher(searcher: GlobalSearchers) { + if (this._currentGlobalSearcher === searcher) return; - this._currentProvider = searchProvider; + this._currentGlobalSearcher = searcher; this.#focusInput(); this._loading = true; @@ -153,8 +171,8 @@ export class UmbSearchModalElement extends UmbLitElement { } async #updateSearchResults() { - if (this._search && this._currentProvider?.api) { - const { data } = await this._currentProvider.api.search({ query: this._search }); + if (this._search && this._currentGlobalSearcher?.api) { + const { data } = await this._currentGlobalSearcher.api.search({ query: this._search }); if (!data) return; this._searchResults = data.items; } else { @@ -325,15 +343,15 @@ export class UmbSearchModalElement extends UmbLitElement { return html`
${repeat( - this._searchProviders, - (searchProvider) => searchProvider.alias, - (searchProvider) => html` + this._globalSearchers, + (searcher) => searcher.alias, + (searcher) => html` `, )} diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/search/types.ts index 176c261cae..3a59d87d6b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/search/types.ts @@ -10,6 +10,7 @@ export type { UmbSearchDataSource } from './search-data-source.interface.js'; export type { UmbSearchRepository } from './search-repository.interface.js'; export type * from './extensions/types.js'; +export type * from './global-search/types.js'; export type UmbSearchResultModel = SearchResultResponseModelReadable; diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.data-source.ts index 3eba1cef8b..5e38b171e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.data-source.ts @@ -4,6 +4,7 @@ import type { StaticFileItemResponseModel } from '@umbraco-cms/backoffice/extern import { StaticFileService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Static File items @@ -21,20 +22,28 @@ export class UmbStaticFileItemServerDataSource extends UmbItemServerDataSourceBa */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } + + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const serializer = new UmbServerFilePathUniqueSerializer(); + const paths = uniques.map((unique) => serializer.toServerPath(unique)!); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => StaticFileService.getItemStaticFile({ query: { path: args.uniques } }), + uniques: paths, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } } -const getItems = (uniques: Array) => { - const serializer = new UmbServerFilePathUniqueSerializer(); - const path = uniques.map((unique) => serializer.toServerPath(unique)!); - - /* eslint-disable local-rules/no-direct-api-import */ - return StaticFileService.getItemStaticFile({ query: { path } }); -}; - const mapper = (item: StaticFileItemResponseModel): UmbStaticFileItemModel => { const serializer = new UmbServerFilePathUniqueSerializer(); return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/sysinfo/components/sysinfo.element.ts b/src/Umbraco.Web.UI.Client/src/packages/sysinfo/components/sysinfo.element.ts index 1cd7ccc38b..43b5c4c8c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/sysinfo/components/sysinfo.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/sysinfo/components/sysinfo.element.ts @@ -5,10 +5,11 @@ import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui'; import { UmbCurrentUserRepository } from '@umbraco-cms/backoffice/current-user'; +import { UmbTemporaryFileConfigRepository } from '@umbraco-cms/backoffice/temporary-file'; type ServerKeyValue = { - name: string; - data: string; + name?: string; + data?: string; }; @customElement('umb-sysinfo') @@ -25,6 +26,7 @@ export class UmbSysinfoElement extends UmbModalBaseElement { readonly #serverKeyValues: Array = []; readonly #sysinfoRepository = new UmbSysinfoRepository(this); readonly #currentUserRepository = new UmbCurrentUserRepository(this); + readonly #temporaryFileConfigRepository = new UmbTemporaryFileConfigRepository(this); override connectedCallback(): void { super.connectedCallback(); @@ -35,28 +37,38 @@ export class UmbSysinfoElement extends UmbModalBaseElement { this._loading = true; this.#serverKeyValues.length = 0; - const [serverTroubleshooting, serverInformation] = await Promise.all([ - this.#sysinfoRepository.requestTroubleShooting(), - this.#sysinfoRepository.requestServerInformation(), - ]); + const [serverTroubleshooting, serverInformation, clientInformation, { data: currentUser }, temporaryFileConfig] = + await Promise.all([ + this.#sysinfoRepository.requestTroubleShooting(), + this.#sysinfoRepository.requestServerInformation(), + this.#sysinfoRepository.requestClientInformation(), + this.#currentUserRepository.requestCurrentUser(), + this.#temporaryFileConfigRepository.requestTemporaryFileConfiguration(), + ]); + this.#serverKeyValues.push({ name: 'Server Troubleshooting' }); if (serverTroubleshooting) { this.#serverKeyValues.push(...serverTroubleshooting.items); } + this.#serverKeyValues.push({}); + this.#serverKeyValues.push({ name: 'Server Information' }); if (serverInformation) { this.#serverKeyValues.push({ name: 'Umbraco build version', data: serverInformation.version }); + this.#serverKeyValues.push({ name: 'Umbraco assembly version', data: serverInformation.assemblyVersion }); this.#serverKeyValues.push({ name: 'Server time offset', data: serverInformation.baseUtcOffset }); this.#serverKeyValues.push({ name: 'Runtime mode', data: serverInformation.runtimeMode }); } - // Browser information - this.#serverKeyValues.push({ name: 'Browser (user agent)', data: navigator.userAgent }); - this.#serverKeyValues.push({ name: 'Browser language', data: navigator.language }); - this.#serverKeyValues.push({ name: 'Browser location', data: location.href }); + this.#serverKeyValues.push({}); + this.#serverKeyValues.push({ name: 'Client Information' }); + if (clientInformation) { + this.#serverKeyValues.push({ name: 'Umbraco client version', data: clientInformation.version }); + } // User information - const { data: currentUser } = await this.#currentUserRepository.requestCurrentUser(); + this.#serverKeyValues.push({}); + this.#serverKeyValues.push({ name: 'Current user' }); if (currentUser) { this.#serverKeyValues.push({ name: 'User is admin', data: currentUser.isAdmin ? 'Yes' : 'No' }); this.#serverKeyValues.push({ name: 'User sections', data: currentUser.allowedSections.join(', ') }); @@ -67,10 +79,39 @@ export class UmbSysinfoElement extends UmbModalBaseElement { }); this.#serverKeyValues.push({ name: 'User document start nodes', - data: currentUser.documentStartNodeUniques.length ? currentUser.documentStartNodeUniques.join(', ') : 'None', + data: currentUser.documentStartNodeUniques.join(', '), }); } + this.#serverKeyValues.push({}); + this.#serverKeyValues.push({ name: 'Temporary file configuration' }); + // Temporary file configuration + if (temporaryFileConfig) { + this.#serverKeyValues.push({ + name: 'Max allowed file size', + data: temporaryFileConfig.maxFileSize?.toString() ?? 'Not set (unlimited)', + }); + this.#serverKeyValues.push({ + name: 'Allowed file types', + data: temporaryFileConfig.allowedUploadedFileExtensions.join(', '), + }); + this.#serverKeyValues.push({ + name: 'Disallowed file types', + data: temporaryFileConfig.disallowedUploadedFilesExtensions?.join(', '), + }); + this.#serverKeyValues.push({ + name: 'Image file types', + data: temporaryFileConfig.imageFileTypes?.join(', '), + }); + } + + // Browser information + this.#serverKeyValues.push({}); + this.#serverKeyValues.push({ name: 'Browser Troubleshooting' }); + this.#serverKeyValues.push({ name: 'Browser (user agent)', data: navigator.userAgent }); + this.#serverKeyValues.push({ name: 'Browser language', data: navigator.language }); + this.#serverKeyValues.push({ name: 'Browser location', data: location.href }); + this._systemInformation = this.#renderServerKeyValues(); this._loading = false; } @@ -78,7 +119,7 @@ export class UmbSysinfoElement extends UmbModalBaseElement { #renderServerKeyValues() { return this.#serverKeyValues .map((serverKeyValue) => { - return `${serverKeyValue.name}: ${serverKeyValue.data}`; + return serverKeyValue.name ? `${serverKeyValue.name}: ${serverKeyValue.data ?? ''}` : ''; }) .join('\n'); } @@ -145,8 +186,9 @@ ${this._systemInformation}`; static override readonly styles = [ UmbTextStyles, css` - #code-block { + #codeblock { max-height: 300px; + overflow: auto; } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/sysinfo/repository/sysinfo.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/sysinfo/repository/sysinfo.repository.ts index c75109ccb1..bfc1453a2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/sysinfo/repository/sysinfo.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/sysinfo/repository/sysinfo.repository.ts @@ -1,3 +1,4 @@ +import packageJson from '../../../../package.json'; import type { UmbServerUpgradeCheck } from '../types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; @@ -20,6 +21,14 @@ export class UmbSysinfoRepository extends UmbRepositoryBase { return data; } + async requestClientInformation() { + const { version } = packageJson; + const clientInformation = { + version, + }; + return clientInformation; + } + /** * Check if the server has an upgrade available and return the result. * If the server has an upgrade available, the result will be stored in local storage. diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/constants.ts index 68cbb2af11..9879ba5d2c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/constants.ts @@ -1,3 +1,4 @@ export * from './conditions/constants.js'; export * from './repository/constants.js'; +export * from './search/constants.js'; export * from './workspace/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/item/template-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/item/template-item.server.data-source.ts index 2e5fc3c7c2..9355611023 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/item/template-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/item/template-item.server.data-source.ts @@ -4,6 +4,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { TemplateItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { TemplateService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Template items @@ -21,14 +22,24 @@ export class UmbTemplateItemServerDataSource extends UmbItemServerDataSourceBase */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => TemplateService.getItemTemplate({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => TemplateService.getItemTemplate({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: TemplateItemResponseModel): UmbTemplateItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/constants.ts new file mode 100644 index 0000000000..c5e079eeb6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/constants.ts @@ -0,0 +1,2 @@ +export * from './global-search/constants.js'; +export const UMB_TEMPLATE_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Template'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/global-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/global-search/constants.ts new file mode 100644 index 0000000000..ae6e9246a4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/global-search/constants.ts @@ -0,0 +1 @@ +export const UMB_TEMPLATE_GLOBAL_SEARCH_ALIAS = 'Umb.GlobalSearch.Template'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/global-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/global-search/manifests.ts new file mode 100644 index 0000000000..f3152b4827 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/global-search/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_TEMPLATE_SEARCH_PROVIDER_ALIAS } from '../constants.js'; +import { UMB_TEMPLATE_GLOBAL_SEARCH_ALIAS } from './constants.js'; +import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; +import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; + +export const manifests: Array = [ + { + name: 'Template Global Search', + alias: UMB_TEMPLATE_GLOBAL_SEARCH_ALIAS, + type: 'globalSearch', + weight: 200, + meta: { + label: 'Templates', + searchProviderAlias: UMB_TEMPLATE_SEARCH_PROVIDER_ALIAS, + }, + conditions: [ + { + alias: UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, + match: UMB_SETTINGS_SECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/manifests.ts index e0acfec7c4..d37a36718f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/search/manifests.ts @@ -1,9 +1,11 @@ import { UMB_TEMPLATE_ENTITY_TYPE } from '../entity.js'; +import { UMB_TEMPLATE_SEARCH_PROVIDER_ALIAS } from './constants.js'; +import { manifests as globalSearchManifests } from './global-search/manifests.js'; export const manifests: Array = [ { name: 'Template Search Provider', - alias: 'Umb.SearchProvider.Template', + alias: UMB_TEMPLATE_SEARCH_PROVIDER_ALIAS, type: 'searchProvider', api: () => import('./template.search-provider.js'), weight: 100, @@ -17,4 +19,5 @@ export const manifests: Array = [ type: 'searchResultItem', forEntityTypes: [UMB_TEMPLATE_ENTITY_TYPE], }, + ...globalSearchManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/media-upload.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/media-upload.tiptap-api.ts index 3d3462a147..0a848e1676 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/media-upload.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/media-upload.tiptap-api.ts @@ -15,13 +15,19 @@ export default class UmbTiptapMediaUploadExtensionApi extends UmbTiptapExtension #configuration?: UmbPropertyEditorConfigCollection; /** - * @returns {number} The maximum width of uploaded images + * @returns {number} The configured maximum allowed image size */ - get maxWidth(): number { + get maxImageSize(): number { const maxImageSize = parseInt(this.#configuration?.getValueByAlias('maxImageSize') ?? '', 10); return isNaN(maxImageSize) ? 500 : maxImageSize; } + /** + * @deprecated Use `maxImageSize` instead. + * @returns {number} The maximum width of uploaded images + */ + maxWidth = this.maxImageSize; + /** * @returns {Array} The allowed mime types for uploads */ @@ -98,7 +104,7 @@ export default class UmbTiptapMediaUploadExtensionApi extends UmbTiptapExtension this.dispatchEvent(new CustomEvent('rte.file.uploading', { composed: true, bubbles: true, detail: fileModels })); const uploads = await this.#manager.upload(fileModels); - const maxImageSize = this.maxWidth; + const maxImageSize = this.maxImageSize; uploads.forEach(async (upload) => { if (upload.status !== TemporaryFileStatus.SUCCESS) { @@ -112,7 +118,11 @@ export default class UmbTiptapMediaUploadExtensionApi extends UmbTiptapExtension } const blobUrl = URL.createObjectURL(upload.file); - const { width, height } = await imageSize(blobUrl, { maxWidth: maxImageSize }); + + // Get the image dimensions - this essentially simulates what the server would do + // when it resizes the image. The server will return the resized image URL. + // We need to use the blob URL here, as the server will not be able to access the local file. + const { width, height } = await imageSize(blobUrl, { maxWidth: maxImageSize, maxHeight: maxImageSize }); editor .chain() diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/media-picker.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/media-picker.tiptap-toolbar-api.ts index ddbd215c82..0963c86be5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/media-picker.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/media-picker.tiptap-toolbar-api.ts @@ -1,6 +1,7 @@ import { UmbTiptapToolbarElementApiBase } from '../base.js'; -import { getGuidFromUdi, getProcessedImageUrl, imageSize } from '@umbraco-cms/backoffice/utils'; +import { getGuidFromUdi, imageSize } from '@umbraco-cms/backoffice/utils'; import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; import { UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, UMB_MEDIA_PICKER_MODAL } from '@umbraco-cms/backoffice/media'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; @@ -8,16 +9,24 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbMediaCaptionAltTextModalValue } from '@umbraco-cms/backoffice/media'; export default class UmbTiptapToolbarMediaPickerToolbarExtensionApi extends UmbTiptapToolbarElementApiBase { + #imagingRepository = new UmbImagingRepository(this); + #modalManager?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; /** - * @returns {number} The maximum width of uploaded images + * @returns {number} The configured maximum allowed image size */ - get maxWidth(): number { + get maxImageSize(): number { const maxImageSize = parseInt(this.configuration?.getValueByAlias('maxImageSize') ?? '', 10); return isNaN(maxImageSize) ? 500 : maxImageSize; } + /** + * @deprecated Use `maxImageSize` instead. + * @returns {number} The maximum width of uploaded images + */ + maxWidth = this.maxImageSize; + constructor(host: UmbControllerHost) { super(host); @@ -98,12 +107,29 @@ export default class UmbTiptapToolbarMediaPickerToolbarExtensionApi extends UmbT async #insertInEditor(editor: Editor, mediaUnique: string, media: UmbMediaCaptionAltTextModalValue) { if (!media?.url) return; - const { width, height } = await imageSize(media.url, { maxWidth: this.maxWidth }); - const src = await getProcessedImageUrl(media.url, { width, height, mode: ImageCropModeModel.MAX }); + const maxImageSize = this.maxImageSize; + + // Get the resized image URL + const { data } = await this.#imagingRepository.requestResizedItems([mediaUnique], { + width: maxImageSize, + height: maxImageSize, + mode: ImageCropModeModel.MAX, + }); + + if (!data?.length || !data[0]?.url) { + console.error('No data returned from imaging repository'); + return; + } + + // Set the media URL to the first item in the data array + const src = data[0].url; + + // Fetch the actual image dimensions + const { width, height } = await imageSize(src); const img = { - alt: media.altText, src, + alt: media.altText, 'data-udi': `umb://media/${mediaUnique.replace(/-/g, '')}`, width: width.toString(), height: height.toString(), diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/action/current-user-app-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/action/current-user-app-button.element.ts index d1aac8ce61..700616a434 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/action/current-user-app-button.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/action/current-user-app-button.element.ts @@ -4,7 +4,7 @@ import type { UmbCurrentUserAction, } from '../current-user-action.extension.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { html, customElement, ifDefined, state, property } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, ifDefined, state, property, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event'; @@ -16,7 +16,7 @@ export class UmbCurrentUserAppButtonElement< #api?: ApiType; @state() - _href?: string; + private _href?: string; @property({ attribute: false }) public manifest?: ManifestCurrentUserActionDefaultKind; @@ -43,13 +43,9 @@ export class UmbCurrentUserAppButtonElement< override render() { return html` - - ${this.manifest?.meta.icon ? html`` : ''} ${this.label} + + ${this.manifest?.meta.icon ? html`` : nothing} + ${this.label} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-action.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-action.extension.ts index 5e1e490376..ff46c69a53 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-action.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-action.extension.ts @@ -1,7 +1,6 @@ import type { UmbAction } from '@umbraco-cms/backoffice/action'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; -import type { UUIInterfaceColor, UUIInterfaceLook } from '@umbraco-cms/backoffice/external/uui'; export interface UmbCurrentUserActionArgs { meta: MetaArgsType; @@ -56,18 +55,6 @@ export interface MetaCurrentUserActionDefaultKind extends MetaCurrentUserAction * ] */ label: string; - - /** - * The look of the button - * @default primary - */ - look?: UUIInterfaceLook; - - /** - * The color of the button - * @default default - */ - color?: UUIInterfaceColor; } declare global { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts index 613bb7bed3..45cb48f931 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts @@ -15,7 +15,6 @@ export const manifests: Array = [ meta: { label: '#defaultdialogs_externalLoginProviders', icon: 'icon-lock', - look: 'secondary', }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/manifests.ts index 4f75473328..93e8964ba9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/mfa-login/manifests.ts @@ -11,7 +11,6 @@ export const manifests: Array = [ meta: { label: '#user_configureTwoFactor', icon: 'icon-rectangle-ellipsis', - look: 'secondary', }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts index 9015d2a72d..460309055d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts @@ -155,6 +155,7 @@ export class UmbUserGroupCollectionTableViewElement extends UmbLitElement { .value=${{ entityType: userGroup.entityType, unique: userGroup.unique, + name: userGroup.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.data-source.ts index fdb9236739..aad5b6ef54 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.data-source.ts @@ -3,6 +3,7 @@ import type { UserGroupItemResponseModel } from '@umbraco-cms/backoffice/externa import { UserGroupService } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for User Group items @@ -20,14 +21,24 @@ export class UmbUserGroupItemServerDataSource extends UmbItemServerDataSourceBas */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => UserGroupService.getItemUserGroup({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => UserGroupService.getItemUserGroup({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: UserGroupItemResponseModel): UmbUserGroupItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts index 9d259b1151..3c1915636e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts @@ -151,6 +151,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { .value=${{ entityType: user.entityType, unique: user.unique, + name: user.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/item/user-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/item/user-item.server.data-source.ts index 18f74820c8..323c08f8a6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/item/user-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/item/user-item.server.data-source.ts @@ -1,6 +1,7 @@ import { UMB_USER_ENTITY_TYPE } from '../../entity.js'; import type { UmbUserItemModel } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; import type { UserItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UserService } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; @@ -18,14 +19,24 @@ export class UmbUserItemServerDataSource extends UmbItemServerDataSourceBase) => UserService.getItemUser({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => UserService.getItemUser({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: UserItemResponseModel): UmbUserItemModel => { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/collection/views/table/webhook-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/collection/views/table/webhook-table-collection-view.element.ts index b65fc8d32b..c9810c2bf3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/collection/views/table/webhook-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/collection/views/table/webhook-table-collection-view.element.ts @@ -109,6 +109,7 @@ export class UmbWebhookTableCollectionViewElement extends UmbLitElement { .value=${{ entityType: webhook.entityType, unique: webhook.unique, + name: webhook.name, }}>`, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.data-source.ts index 91775f3ac7..f6115c194f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.data-source.ts @@ -3,6 +3,7 @@ import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository' import type { WebhookItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { WebhookService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A server data source for Webhook items @@ -20,14 +21,24 @@ export class UmbWebhookItemServerDataSource extends UmbItemServerDataSourceBase< */ constructor(host: UmbControllerHost) { super(host, { - getItems, mapper, }); } -} -/* eslint-disable local-rules/no-direct-api-import */ -const getItems = (uniques: Array) => WebhookService.getItemWebhook({ query: { id: uniques } }); + override async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const itemRequestManager = new UmbItemDataApiGetRequestController(this, { + // eslint-disable-next-line local-rules/no-direct-api-import + api: (args) => WebhookService.getItemWebhook({ query: { id: args.uniques } }), + uniques, + }); + + const { data, error } = await itemRequestManager.request(); + + return { data: this._getMappedItems(data), error }; + } +} const mapper = (item: WebhookItemResponseModel): UmbWebhookItemModel => { return { diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index e7ca8508f0..24377bca5b 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.33", - "@umbraco/playwright-testhelpers": "^16.0.11", + "@umbraco/playwright-testhelpers": "^16.0.15", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -66,9 +66,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "16.0.11", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.11.tgz", - "integrity": "sha512-jduJC8xqtqQ78Sata3GhafDLavRv0ZaKHKFwz3KdLw0VmLNxgDMABAV0SMFGU9sABGzi3MjEUeVQ4ntH1nvA3w==", + "version": "16.0.15", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.15.tgz", + "integrity": "sha512-qrhVUy0EeSnOL9SNxLNmqdl+SWXamDuPWxDLMWE6f3B1DW4DDTjJYHY+GeYdP4nbXT6OJKtSSH1jCDJi7Ofkeg==", "license": "MIT", "dependencies": { "@umbraco/json-models-builders": "2.0.33", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 460c45b9b7..a77b72afd2 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.33", - "@umbraco/playwright-testhelpers": "^16.0.11", + "@umbraco/playwright-testhelpers": "^16.0.15", "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 5a784172c2..b7f9fc6ac8 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts @@ -33,6 +33,7 @@ export default defineConfig({ // When working locally it can be a good idea to use trace: 'on-first-retry' instead of 'retain-on-failure', it can cut the local test times in half. trace: 'retain-on-failure', ignoreHTTPSErrors: true, + testIdAttribute: 'data-mark' }, /* Configure projects for major browsers */ diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ComplexBlockGridTest.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ComplexBlockGridTest.spec.ts new file mode 100644 index 0000000000..652065835e --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ComplexBlockGridTest.spec.ts @@ -0,0 +1,137 @@ +import {expect} from '@playwright/test'; +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +// DocumentType +const documentTypeName = 'TestDocumentType'; +let documentTypeId = ''; +const groupName = 'TestGroup'; + +// Content +const contentName = 'TestContent'; +let contentId = ''; + +// Property Value +const wrongPropertyValue = 'This is a test with wrong value**'; +const correctPropertyValue = 'Test'; + +// ElementTypes +// TextString Element Type (for Block List) +const textStringElementGroupName = 'TextStringElementGroup'; +const textStringElementTypeName = 'TestElementWithTextString'; +const textStringElementRegex = '^[a-zA-Z0-9]*$'; +let textStringElementTypeId = ''; +// Area Element Type (for Block Grid) +const areaElementTypeName = 'TestElementArea'; +const areaAlias = 'testArea'; +let areaElementTypeId = ''; +// Rich Text Editor Element Type (for Block Grid) +const richTextEditorElementGroupName = 'RichTextEditorElementGroup'; +const richTextEditorElementTypeName = 'RichTextEditorTestElement'; +let richTextEditorElementTypeId = ''; +// Block List Element Type +const blockListElementTypeName = 'BlockListElement'; +const blockListGroupName = 'BlockListGroup'; +let blockListElementTypeId = ''; + +// DataTypes +const blockGridDataTypeName = 'TestBlockGridEditor'; +const blockListDataTypeName = 'TestBlockListEditor'; +const textStringElementDataTypeName = 'Textstring'; +const richTextDataTypeName = 'Rich Text Editor'; +let blockListDataTypeId = ''; +let blockGridDataTypeId = ''; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(areaElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextEditorElementTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(areaElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextEditorElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); +}); + +// Needs to be updated as we are not asserting on save or publish notifications as we did in 15 +test.fixme('can update property value nested in a block grid area with an RTE with a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + test.slow(); + // Arrange + // ElementType with Textstring And REGEX only accept letters and numbers + const textStringElementDataType = await umbracoApi.dataType.getByName(textStringElementDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createElementTypeWithRegexValidation(textStringElementTypeName, textStringElementGroupName, textStringElementDataTypeName, textStringElementDataType.id, textStringElementRegex); + // Block List Editor with Textstring + blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId); + // ElementType with Block List Editor + blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListGroupName, blockListDataTypeName, blockListDataTypeId); + // Rich Text Editor in an ElementType, with a Block(Element Type), the block contains a Block List Editor + const richTextEditorId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockListElementTypeId); + richTextEditorElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextEditorElementTypeName, richTextEditorElementGroupName, richTextDataTypeName, richTextEditorId); + // ElementType Area that is Empty + areaElementTypeId = await umbracoApi.documentType.createEmptyElementType(areaElementTypeName); + // Block Grid with 2 blocks, one with RTE and Inline, and one with areas + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockWithInlineEditingModeAndABlockWithAnArea(blockGridDataTypeName, richTextEditorElementTypeId, true, areaElementTypeId, areaAlias); + // Document Type with the following + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, groupName); + // Creates Content + contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickAddBlockGridElementWithName(areaElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(areaElementTypeName); + await umbracoUi.content.clickAddBlockGridElementWithName(richTextEditorElementTypeName); + await umbracoUi.content.clickExactLinkWithName(richTextEditorElementTypeName); + await umbracoUi.content.clickInsertBlockButton(); + await umbracoUi.content.clickExactLinkWithName(blockListElementTypeName); + await umbracoUi.content.clickAddBlockGridElementWithName(textStringElementTypeName); + await umbracoUi.content.clickExactLinkWithName(textStringElementTypeName); + // Enter text in the textstring block that won't match regex + await umbracoUi.content.enterPropertyValue(textStringElementDataTypeName, wrongPropertyValue); + await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(textStringElementTypeName, textStringElementGroupName); + await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(blockListElementTypeName, blockListGroupName); + await umbracoUi.content.clickCreateButtonForModalWithElementTypeNameAndGroupName(richTextEditorElementTypeName, richTextEditorElementGroupName); + await umbracoUi.content.clickSaveAndPublishButton(); + // Checks that the error notification is shown since the textstring block has the wrong value + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved, true, true); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished, true, true); + // Updates the textstring block with the correct value + await umbracoUi.waitForTimeout(1000); + await umbracoUi.content.clickBlockElementWithName(blockListElementTypeName); + await umbracoUi.content.clickEditBlockListEntryWithName(textStringElementTypeName); + await umbracoUi.content.enterPropertyValue(textStringElementDataTypeName, correctPropertyValue); + await umbracoUi.content.clickUpdateButtonForModalWithElementTypeNameAndGroupName(textStringElementTypeName, textStringElementGroupName); + await umbracoUi.content.clickUpdateButtonForModalWithElementTypeNameAndGroupName(blockListElementTypeName, blockListGroupName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + // await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved, true, true); + // await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published, true, true); + await umbracoUi.content.isErrorNotificationVisible(false); + await umbracoUi.waitForTimeout(1000); + // Checks if published + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe('Published'); + // Checks if the textstring block has the correct value after reloading the page + await umbracoUi.reloadPage(); + // Waits to make sure the page has loaded + await umbracoUi.waitForTimeout(2000); + await umbracoUi.content.clickBlockElementWithName(blockListElementTypeName); + // Needs to wait to make sure it has loaded + await umbracoUi.waitForTimeout(2000); + await umbracoUi.content.clickEditBlockListEntryWithName(textStringElementTypeName); + await umbracoUi.content.doesPropertyContainValue(textStringElementDataTypeName, correctPropertyValue); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts index 9b7707009d..6643c93cd9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts @@ -33,7 +33,7 @@ test('can create content with an empty block grid', async ({umbracoApi, umbracoU // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -57,7 +57,7 @@ test('can publish content with an empty block grid', async ({umbracoApi, umbraco // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveAndPublishButton(); @@ -283,7 +283,7 @@ test('can create content with a block grid with the inline editing mode enabled' // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/SecondLevelBlockProperties.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/SecondLevelBlockProperties.spec.ts new file mode 100644 index 0000000000..292d7c96d3 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/SecondLevelBlockProperties.spec.ts @@ -0,0 +1,161 @@ +import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +// Content Name +const contentName = 'ContentName'; + +// Document Type +const documentTypeName = 'DocumentTypeName'; +let documentTypeId = null; +const documentTypeGroupName = 'DocumentGroup'; + +// Block Grid +const blockGridDataTypeName = 'BlockGridName'; +let blockGridDataTypeId = null; + +// Text String +const textStringElementTypeName = 'TextStringElementName'; +let textStringElementTypeId = null; +let textStringGroupName = 'TextGroup'; +const textStringDataTypeName = 'Textstring'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); +}); + +test('can publish a block grid editor with a rich text editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const richTextEditorValue = 'Hello World'; + const expectedRichTextEditorOutputValue = '

Hello World

'; + const richTextDataTypeName = 'RichTextDataTypeName'; + const richTextElementTypeName = 'RichTextElementName'; + const richTextElementGroupName = 'RichTextElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName); + + const richTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(richTextDataTypeName); + const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, richTextDataTypeName, richTextEditorDataTypeId); + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, richTextElementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true); + await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the RTE is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.markup).toContain(expectedRichTextEditorOutputValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName); +}); + +test('can publish a block grid editor with a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const blockListDataTypeName = 'BlockListName'; + const blockListElementTypeName = 'BlockListElementName'; + const blockListElementGroupName = 'BlockListElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId); + const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, blockListDataTypeName, blockListDataTypeId); + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, blockListElementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockList is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); +}); + +test('can publish a block grid editor with a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const secondBlockGridDataTypeName = 'SecondBlockGridDataTypeName'; + const blockGridElementTypeName = 'BlockGridElementTypeName'; + const blockGridElementGroupName = 'BlockGridElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(secondBlockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const secondBlockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(secondBlockGridDataTypeName, textStringElementTypeId); + const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, secondBlockGridDataTypeName, secondBlockGridDataTypeId); + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, blockGridElementTypeId, true); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockGrid is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockGridDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(secondBlockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts index e06dbcd721..f7df86fb98 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts @@ -33,7 +33,7 @@ test('can create content with an empty block list', async ({umbracoApi, umbracoU // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -57,7 +57,7 @@ test('can publish content with an empty block list', async ({umbracoApi, umbraco // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveAndPublishButton(); @@ -232,7 +232,7 @@ test('can create content with a block list with the inline editing mode enabled' // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/SecondLevelBlockProperties.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/SecondLevelBlockProperties.spec.ts new file mode 100644 index 0000000000..ac3dd0aefd --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/SecondLevelBlockProperties.spec.ts @@ -0,0 +1,161 @@ +import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +// Content Name +const contentName = 'ContentName'; + +// Document Type +const documentTypeName = 'DocumentTypeName'; +let documentTypeId = null; +const documentTypeGroupName = 'DocumentGroup'; + +// Block List +const blockListDataTypeName = 'BlockListName'; +let blockListDataTypeId = null; + +// Text String +const textStringElementTypeName = 'TextStringElementName'; +let textStringElementTypeId = null; +let textStringGroupName = 'TextGroup'; +const textStringDataTypeName = 'Textstring'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); +}); + +test('can publish a block list editor with a rich text editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const richTextEditorValue = 'Hello World'; + const expectedRichTextEditorOutputValue = '

Hello World

'; + const richTextDataTypeName = 'RichTextName'; + const richTextElementTypeName = 'RichTextElementName'; + const richTextElementGroupName = 'RTEElementGroup'; + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName); + + const richTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(richTextDataTypeName); + const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, richTextDataTypeName, richTextEditorDataTypeId); + blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, richTextElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true); + await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the RTE is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentRichTextValues = documentData.values[0].value.contentData[0].values.find(value => value.alias === AliasHelper.toAlias(richTextDataTypeName)); + expect(documentRichTextValues.value.markup).toContain(expectedRichTextEditorOutputValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementTypeName); +}); + +test('can publish a block list editor with a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const blockGridDataTypeName = 'BlockGridDataTypeName'; + const blockGridElementTypeName = 'BlockGridElementName'; + const blockGridElementGroupName = 'GridElementGroup'; + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridDataTypeName, textStringElementTypeId); + const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, blockGridDataTypeName, blockGridDataTypeId); + blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, blockGridElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockGrid is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockListDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeId); +}); + +test('can publish a block list editor with a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const secondBlockListDataTypeName = 'SecondBlockListName'; + const blockListElementTypeName = 'BlockListElementName'; + const blockListElementGroupName = 'ListElementGroup'; + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.dataType.ensureNameNotExists(secondBlockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const secondBlockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(secondBlockListDataTypeName, textStringElementTypeId); + const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, secondBlockListDataTypeName, secondBlockListDataTypeId); + blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, blockListElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListDataTypeName, blockListDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockList is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(blockListDataTypeName)); + expect(documentValues.value.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(secondBlockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); +}); 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 1a4e1495f7..7cca397f75 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts @@ -31,7 +31,7 @@ test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(childDocumentTypeName); await umbracoUi.content.enterContentName(childContentName); await umbracoUi.content.clickSaveButton(); @@ -44,7 +44,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.clickReloadChildrenButton(); + await umbracoUi.content.clickReloadChildrenActionMenuOption(); await umbracoUi.content.clickCaretButtonForContentName(contentName); await umbracoUi.content.doesContentTreeHaveName(childContentName); @@ -70,7 +70,7 @@ test('can create child node in child node', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickCaretButtonForContentName(contentName); await umbracoUi.content.clickActionsMenuForContent(childContentName); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(childOfChildDocumentTypeName); // This wait is needed await umbracoUi.content.enterContentName(childOfChildContentName); @@ -102,7 +102,7 @@ test('cannot publish child if the parent is not published', async ({umbracoApi, // Act await umbracoUi.content.clickCaretButtonForContentName(contentName); await umbracoUi.content.clickActionsMenuForContent(childContentName); - await umbracoUi.content.clickPublishButton(); + await umbracoUi.content.clickPublishActionMenuOption(); await umbracoUi.content.clickConfirmToPublishButton(); // Assert @@ -122,7 +122,7 @@ test('can publish content with child node', {tag: '@smoke'}, async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickPublishButton(); + await umbracoUi.content.clickPublishActionMenuOption(); await umbracoUi.content.clickConfirmToPublishButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockGridBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockGridBlocks.spec.ts index 4b0e91560f..866cbe9959 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockGridBlocks.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockGridBlocks.spec.ts @@ -267,7 +267,7 @@ test('can replace multiple blocks', async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.clickPasteButton(); await umbracoUi.content.goToBlockGridBlockWithName(groupName, blockGridDataTypeName, elementTypeName, 0); await umbracoUi.content.doesBlockEditorBlockWithNameContainValue(elementGroupName, elementPropertyName, ConstantHelper.inputTypes.tipTap, blockPropertyValue); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateModalButton(); await umbracoUi.content.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockListBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockListBlocks.spec.ts index c3ac6c1b05..d56e2e0ac7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockListBlocks.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Clipboard/ClipboardBlockListBlocks.spec.ts @@ -267,7 +267,7 @@ test('can replace multiple blocks', async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.clickPasteButton(); await umbracoUi.content.goToBlockListBlockWithName(groupName, blockListDataTypeName, elementTypeName, 0); await umbracoUi.content.doesBlockEditorBlockWithNameContainValue(elementGroupName, elementPropertyName, ConstantHelper.inputTypes.tipTap, blockPropertyValue); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateModalButton(); await umbracoUi.content.clickSaveButton(); // Assert 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 4bd1f27409..6c98206c2e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Content.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Content.spec.ts @@ -26,7 +26,7 @@ test('can create empty content', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -48,7 +48,7 @@ test('can save and publish empty content', {tag: '@smoke'}, async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveAndPublishButton(); @@ -71,7 +71,7 @@ test('can create content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.enterTextstring(contentText); @@ -138,7 +138,7 @@ test('can publish invariant content node', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickPublishButton(); + await umbracoUi.content.clickPublishActionMenuOption(); await umbracoUi.content.clickConfirmToPublishButton(); // Assert @@ -158,7 +158,7 @@ test('can unpublish content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickUnpublishButton(); + await umbracoUi.content.clickUnpublishActionMenuOption(); await umbracoUi.content.clickConfirmToUnpublishButton(); // Assert @@ -177,7 +177,7 @@ test('can publish variant content node', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickPublishButton(); + await umbracoUi.content.clickPublishActionMenuOption(); await umbracoUi.content.clickConfirmToPublishButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithApprovedColor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithApprovedColor.spec.ts index 9a785f6549..71be717fd3 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithApprovedColor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithApprovedColor.spec.ts @@ -25,7 +25,7 @@ test('can create content with the approved color data type', async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts index 315c974fd4..6d40370d27 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts @@ -28,7 +28,7 @@ test('can create content with the checkbox list data type', async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -99,7 +99,6 @@ test('can not publish a mandatory checkbox list with an empty value', async ({um // Do not select any checkbox list values and the validation error appears await umbracoUi.content.clickSaveAndPublishButton(); await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); - await umbracoUi.content.isErrorNotificationVisible(); await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); // Select a checkbox list value and the validation error disappears await umbracoUi.content.chooseCheckboxListOption(optionValues[0]); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts index 2fe3f6cec6..d34d387baa 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithContentPicker.spec.ts @@ -32,7 +32,7 @@ test('can create content with the content picker datatype', {tag: '@smoke'}, asy // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.addContentPicker(contentPickerName); 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 2a6ed55a13..4d377775ef 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCustomDataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCustomDataType.spec.ts @@ -26,7 +26,7 @@ test('can create content with the custom data type with email address property e // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -73,7 +73,7 @@ test('can create content with the custom data type with decimal property editor' // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -83,7 +83,8 @@ test('can create content with the custom data type with decimal property editor' await umbracoUi.content.isErrorNotificationVisible(false); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values).toEqual([]); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value).toEqual(0); }); test('can add decimal number to the decimal in the content section', async ({umbracoApi, umbracoUi}) => { @@ -121,7 +122,7 @@ test.skip('can create content with the custom data type with code editor propert // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -168,7 +169,7 @@ test('can create content with the custom data type with markdown editor property // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -215,7 +216,7 @@ test('can create content with the custom data type with multiple text string pro // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -263,7 +264,7 @@ test('can create content with the custom data type with slider property editor', // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowAtRoot.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowAtRoot.spec.ts index cc1b17b9a4..0d450f08f1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowAtRoot.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowAtRoot.spec.ts @@ -18,7 +18,7 @@ test('cannot create content if allow at root is disabled', async ({umbracoApi, u // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); // Assert await umbracoUi.content.isDocumentTypeNameVisible(documentTypeName, false); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowVaryByCulture.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowVaryByCulture.spec.ts index 427d7a7b54..48fd420b46 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowVaryByCulture.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowVaryByCulture.spec.ts @@ -26,11 +26,11 @@ test('can create content with allow vary by culture enabled', async ({umbracoApi // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButtonForContent(); - await umbracoUi.content.clickSaveAndCloseButton(); + await umbracoUi.content.clickSaveButton(); // Assert //await umbracoUi.content.isSuccessNotificationVisible(); @@ -52,10 +52,9 @@ test('can create content with names that vary by culture', async ({umbracoApi, u await umbracoUi.content.clickVariantAddModeButtonForLanguageName(secondLanguageName); await umbracoUi.content.enterContentName(danishContentName); await umbracoUi.content.clickSaveButtonForContent(); - await umbracoUi.content.clickSaveAndCloseButton(); + await umbracoUi.content.clickSaveButton(); // Assert - //await umbracoUi.content.isSuccessNotificationVisible(); await umbracoUi.content.isErrorNotificationVisible(false); expect(await umbracoApi.document.doesNameExist(danishContentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(danishContentName); @@ -68,7 +67,6 @@ test('can create content with names that vary by culture and content that is inv // Arrange const danishContentName = 'Test indhold'; const textContent = 'This is a test text'; - const danishTextContent = 'Dette er testtekst'; const dataTypeName = 'Textstring'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, 'Test Group', true, false); @@ -81,12 +79,10 @@ test('can create content with names that vary by culture and content that is inv await umbracoUi.content.clickSelectVariantButton(); await umbracoUi.content.clickVariantAddModeButtonForLanguageName(secondLanguageName); await umbracoUi.content.enterContentName(danishContentName); - await umbracoUi.content.enterTextstring(danishTextContent); await umbracoUi.content.clickSaveButtonForContent(); - await umbracoUi.content.clickSaveAndCloseButton(); + await umbracoUi.content.clickSaveButton(); // Assert - //await umbracoUi.content.isSuccessNotificationVisible(); await umbracoUi.content.isErrorNotificationVisible(false); expect(await umbracoApi.document.doesNameExist(danishContentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(danishContentName); @@ -94,7 +90,7 @@ test('can create content with names that vary by culture and content that is inv expect(contentData.variants[0].name).toBe(contentName); expect(contentData.variants[1].name).toBe(danishContentName); expect(contentData.values.length).toBe(1); - expect(contentData.values[0].value).toBe(danishTextContent); + expect(contentData.values[0].value).toBe(textContent); }); test('can create content with names and content that vary by culture', async ({umbracoApi, umbracoUi}) => { @@ -116,7 +112,7 @@ test('can create content with names and content that vary by culture', async ({u await umbracoUi.content.enterContentName(danishContentName); await umbracoUi.content.enterTextstring(danishTextContent); await umbracoUi.content.clickSaveButtonForContent(); - await umbracoUi.content.clickSaveAndCloseButton(); + await umbracoUi.content.clickSaveButton(); // Assert //await umbracoUi.content.isSuccessNotificationVisible(); 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 bce562e334..6e43246c17 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 @@ -25,7 +25,7 @@ test('can create content with allowed child node enabled', async ({umbracoApi, u // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -48,7 +48,7 @@ test('cannot create child content if allowed child node is disabled', async ({um // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); // Assert await umbracoUi.content.isDocumentTypeNameVisible(documentTypeName, false); @@ -70,7 +70,7 @@ test('can create multiple child nodes with different document types', async ({um // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(secondChildDocumentTypeName); await umbracoUi.content.enterContentName(secondChildContentName); await umbracoUi.content.clickSaveButton(); @@ -85,7 +85,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.clickReloadChildrenButton(); + await umbracoUi.content.clickReloadChildrenActionMenuOption(); 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/ContentWithAllowedTemplates.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedTemplates.spec.ts index 73abbe9caf..df544f016f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedTemplates.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedTemplates.spec.ts @@ -27,7 +27,7 @@ test('can create content with an allowed template', async ({umbracoApi, umbracoU // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -51,7 +51,7 @@ test('can create content with multiple allowed templates', async ({umbracoApi, u // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); 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 c0eec3f876..425a2e6451 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 @@ -28,7 +28,7 @@ test('can create content configured as a collection', async ({umbracoApi, umbrac // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -52,7 +52,7 @@ test('can create child content in a collection', async ({umbracoApi, umbracoUi}) // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(childDocumentTypeName); await umbracoUi.content.enterContentName(firstChildContentName); await umbracoUi.content.clickSaveButton(); @@ -63,7 +63,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.clickReloadChildrenButton(); + await umbracoUi.content.clickReloadChildrenActionMenuOption(); await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.doesDocumentTableColumnNameValuesMatch(expectedNames); @@ -84,7 +84,7 @@ test('can create multiple child nodes in a collection', async ({umbracoApi, umbr // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(childDocumentTypeName); await umbracoUi.content.enterContentName(secondChildContentName); await umbracoUi.content.clickSaveButton(); @@ -96,7 +96,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.clickReloadChildrenButton(); + await umbracoUi.content.clickReloadChildrenActionMenuOption(); await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.doesDocumentTableColumnNameValuesMatch(expectedNames); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts index dbe5c4b75d..db1f1b4d88 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts @@ -29,7 +29,7 @@ for (const dataTypeName of dataTypeNames) { // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -98,7 +98,6 @@ test('can not publish a mandatory dropdown with an empty value', async ({umbraco // Do not select any dropdown values and the validation error appears await umbracoUi.content.clickSaveAndPublishButton(); await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); - await umbracoUi.content.isErrorNotificationVisible(); await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); // Select a dropdown value and the validation error disappears await umbracoUi.content.chooseDropdownOption([optionValues[0]]); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts index 7d69687595..26e0099cd5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts @@ -31,7 +31,7 @@ test('can create content with the image cropper data type', {tag: '@smoke'}, asy // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.uploadFile(imageFilePath); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts index aa003decf9..a45b146cc6 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts @@ -28,7 +28,7 @@ test('can save content with a image media picker', async ({umbracoApi, umbracoUi // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithListViewContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithListViewContent.spec.ts index 5a84e65a44..0a0b09e163 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithListViewContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithListViewContent.spec.ts @@ -22,8 +22,8 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.documentType.ensureNameNotExists(childDocumentTypeName); }); -// Remove .fixme when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 -test.fixme('can create content with the list view data type', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can create content with the list view data type', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedState = 'Draft'; const defaultListViewDataTypeName = 'List View - Content'; @@ -34,7 +34,7 @@ test.fixme('can create content with the list view data type', async ({umbracoApi // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -48,7 +48,8 @@ test.fixme('can create content with the list view data type', async ({umbracoApi expect(await umbracoApi.document.getChildrenAmount(contentData.id)).toEqual(0); }); -test('can publish content with the list view data type', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can publish content with the list view data type', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedState = 'Published'; const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); @@ -71,7 +72,8 @@ test('can publish content with the list view data type', async ({umbracoApi, umb expect(await umbracoApi.document.getChildrenAmount(documentId)).toEqual(0); }); -test('can create content with a child in the list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can create content with a child in the list', async ({umbracoApi, umbracoUi}) => { // Arrange const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); await umbracoApi.dataType.createListViewContentDataType(dataTypeName); @@ -92,7 +94,8 @@ test('can create content with a child in the list', async ({umbracoApi, umbracoU expect(await umbracoApi.document.getChildrenAmount(documentId)).toEqual(1); }); -test('can publish content with a child in the list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can publish content with a child in the list', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedState = 'Published'; const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); @@ -121,7 +124,8 @@ test('can publish content with a child in the list', async ({umbracoApi, umbraco expect(childContentData.variants[0].state).toBe(expectedState); }); -test('can not publish child in a list when parent is not published', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can not publish child in a list when parent is not published', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedState = 'Draft'; const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); @@ -148,7 +152,8 @@ test('can not publish child in a list when parent is not published', async ({umb expect(childContentData.variants[0].state).toBe(expectedState); }); -test('child is removed from list after child content is deleted', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('child is removed from list after child content is deleted', async ({umbracoApi, umbracoUi}) => { // Arrange const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); await umbracoApi.dataType.createListViewContentDataType(dataTypeName); @@ -162,7 +167,7 @@ test('child is removed from list after child content is deleted', async ({umbrac // Act await umbracoUi.content.clickCaretButtonForContentName(contentName); await umbracoUi.content.clickActionsMenuForContent(childContentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); await umbracoUi.content.clickConfirmTrashButton(); // Assert @@ -172,7 +177,8 @@ test('child is removed from list after child content is deleted', async ({umbrac expect(await umbracoApi.document.doesNameExist(childContentName)).toBeFalsy(); }); -test('can sort list by name', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can sort list by name', async ({umbracoApi, umbracoUi}) => { // Arrange const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); const secondChildContentName = 'ASecondChildContent'; @@ -195,7 +201,8 @@ test('can sort list by name', async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.doesFirstItemInListViewHaveName(secondChildContentName); }); -test('can publish child content from list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can publish child content from list', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedState = 'Published'; const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); @@ -220,7 +227,8 @@ test('can publish child content from list', async ({umbracoApi, umbracoUi}) => { expect(childContentData.variants[0].state).toBe(expectedState); }); -test('can not publish child content from list when parent is not published', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can not publish child content from list when parent is not published', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedState = 'Draft'; const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); @@ -242,7 +250,8 @@ test('can not publish child content from list when parent is not published', asy expect(childContentData.variants[0].state).toBe(expectedState); }); -test('can unpublish child content from list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can unpublish child content from list', async ({umbracoApi, umbracoUi}) => { // Arrange const expectedState = 'Draft'; const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); @@ -270,7 +279,8 @@ test('can unpublish child content from list', async ({umbracoApi, umbracoUi}) => expect(childContentData.variants[0].state).toBe(expectedState); }); -test('can duplicate child content in list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can duplicate child content in list', async ({umbracoApi, umbracoUi}) => { // Arrange const secondDocumentName = 'SecondDocument'; await umbracoApi.document.ensureNameNotExists(secondDocumentName); @@ -301,7 +311,8 @@ test('can duplicate child content in list', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.document.getChildrenAmount(secondDocumentId)).toEqual(1); }); -test('can move child content in list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can move child content in list', async ({umbracoApi, umbracoUi}) => { // Arrange const secondDocumentName = 'SecondDocument'; await umbracoApi.document.ensureNameNotExists(secondDocumentName); @@ -332,7 +343,8 @@ test('can move child content in list', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.document.getChildrenAmount(secondDocumentId)).toEqual(1); }); -test('can trash child content in list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can trash child content in list', async ({umbracoApi, umbracoUi}) => { // Arrange const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); await umbracoApi.dataType.createListViewContentDataTypeWithAllPermissions(dataTypeName); @@ -356,7 +368,8 @@ test('can trash child content in list', async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.isItemVisibleInRecycleBin(childContentName); }); -test('can search for child content in list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can search for child content in list', async ({umbracoApi, umbracoUi}) => { // Arrange const secondChildName = 'SecondChildDocument'; await umbracoApi.document.ensureNameNotExists(secondChildName); @@ -379,7 +392,8 @@ test('can search for child content in list', async ({umbracoApi, umbracoUi}) => await umbracoUi.content.doesFirstItemInListViewHaveName(childContentName); }); -test('can change from list view to grid view in list', async ({umbracoApi, umbracoUi}) => { +// Remove .skip when the issue is fixed: https://github.com/umbraco/Umbraco-CMS/issues/18615 +test.skip('can change from list view to grid view in list', async ({umbracoApi, umbracoUi}) => { // Arrange const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); await umbracoApi.dataType.createListViewContentDataTypeWithAllPermissions(dataTypeName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts index f09875958c..9b71b6650e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts @@ -31,7 +31,7 @@ test('can create content with the media picker data type', {tag: '@smoke'}, asyn // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaFileName); @@ -61,7 +61,7 @@ test('can publish content with the media picker data type', async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaFileName); @@ -132,7 +132,7 @@ test('can limit the media picker in the content by setting the start node', asyn await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); }); -test('can not publish a mandatory media picker with an empty value', async ({umbracoApi, umbracoUi}) => { +test.fixme('can not publish a mandatory media picker with an empty value', async ({umbracoApi, umbracoUi}) => { // Arrange const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, 'Test Group', false, false, true); @@ -145,9 +145,9 @@ test('can not publish a mandatory media picker with an empty value', async ({umb // Do not pick any media and the validation error appears await umbracoUi.content.clickSaveAndPublishButton(); await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); - await umbracoUi.content.isErrorNotificationVisible(); await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); // Pick a media value and the validation error disappears + // TODO: This should be fixed await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaFileName); await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts index 5718d526aa..38bd38fde4 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMemberPicker.spec.ts @@ -35,7 +35,7 @@ test('can create content with the member picker data type', {tag: '@smoke'}, asy // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickChooseMemberPickerButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts index b8c53efdac..25e3f113ec 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts @@ -33,7 +33,7 @@ test('can create content with the document link', {tag: '@smoke'}, async ({umbra // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickAddMultiURLPickerButton(); @@ -375,7 +375,7 @@ test.skip('can create content with the link to an unpublished document', async ( // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickAddMultiURLPickerButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts index f5644dd560..8a225b6e8d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts @@ -36,7 +36,7 @@ test('can create content with multiple image media picker data type', async ({um // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts index ab669937bc..41041ff70c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts @@ -37,7 +37,7 @@ test('can create content with multiple media picker data type', async ({umbracoA // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts index 25f9abd5b5..a22a03affd 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithNumeric.spec.ts @@ -26,7 +26,7 @@ test('can create content with the numeric data type', async ({umbracoApi, umbrac // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.enterNumeric(number); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts index c771c0bb64..e91e525c6f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithPropertyEditors.spec.ts @@ -26,7 +26,7 @@ test.skip('can create content with the Rich Text Editor datatype', {tag: '@smoke // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.enterRichTextArea(contentText); @@ -52,7 +52,7 @@ test.skip('can create content with the upload file datatype', async ({umbracoApi // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.uploadFile('./fixtures/mediaLibrary/' + uploadFilePath); @@ -79,7 +79,7 @@ test.skip('can create content with the list view - content datatype', async ({um // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); // TODO: add step to interact with the list diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts index e1f0d8108c..94a2545c30 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts @@ -29,7 +29,7 @@ test('can create content with the radiobox data type', async ({umbracoApi, umbra // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -101,7 +101,6 @@ test('can not publish mandatory radiobox with an empty value', async ({umbracoAp // Do not select any radiobox values and the validation error appears await umbracoUi.content.clickSaveAndPublishButton(); await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue); - await umbracoUi.content.isErrorNotificationVisible(); await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); // Select a radiobox value and the validation error disappears await umbracoUi.content.chooseRadioboxOption(optionValues[0]); @@ -109,7 +108,6 @@ test('can not publish mandatory radiobox with an empty value', async ({umbracoAp await umbracoUi.content.clickSaveAndPublishButton(); // Assert - await umbracoUi.content.isErrorNotificationVisible(); await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts index fc72f4f236..ff316715ed 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTags.spec.ts @@ -26,7 +26,7 @@ test('can create content with one tag', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickPlusIconButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts index e568d621b9..c9112c583e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts @@ -27,7 +27,7 @@ test('can create content with the textarea data type', async ({umbracoApi, umbra // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -100,7 +100,7 @@ test('cannot input the text that exceeds the allowed amount of characters', asyn // Assert await umbracoUi.content.isTextWithMessageVisible(warningMessage); - //await umbracoUi.content.isSuccessNotificationVisible(); + // await umbracoUi.content.isSuccessNotificationVisible(); await umbracoUi.content.isErrorNotificationVisible(false); // Clean diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts index e44d1517a8..64fdf72ee6 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts @@ -27,7 +27,7 @@ test('can create content with the textstring data type', async ({umbracoApi, umb // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -89,6 +89,7 @@ test('cannot input the text that exceeds the allowed amount of characters', asyn const textExceedMaxChars = 'Lorem ipsum dolor sit'; const warningMessage = 'The string length exceeds the maximum length of'; const dataTypeId = await umbracoApi.dataType.createTextstringDataType(customDataTypeName, maxChars); + console.log(dataTypeId); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); 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 504dfea783..1789af90fc 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts @@ -26,7 +26,7 @@ test('can create content with the true/false data type', async ({umbracoApi, umb // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -37,7 +37,8 @@ test('can create content with the true/false data type', async ({umbracoApi, umb expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); - expect(contentData.values).toEqual([]); + expect(contentData.values[0].alias).toEqual('truefalse'); + expect(contentData.values[0].value).toEqual(false); }); test('can publish content with the true/false data type', async ({umbracoApi, umbracoUi}) => { @@ -93,7 +94,7 @@ test('can toggle the true/false value with the initial state enabled', async ({u // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickToggleButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts index 05731bfde1..2d255fdc03 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadArticle.spec.ts @@ -26,7 +26,7 @@ test('can create content with the upload article data type', async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts index 7d6ebc23f1..f3e7064557 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadAudio.spec.ts @@ -26,7 +26,7 @@ test('can create content with the upload audio data type', async ({umbracoApi, u // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts index 8b6477a6e3..a6f45b6ad4 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadFile.spec.ts @@ -26,7 +26,7 @@ test('can create content with the upload file data type', async ({umbracoApi, um // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVectorGraphics.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVectorGraphics.spec.ts index 7a46b05420..da8557d514 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVectorGraphics.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVectorGraphics.spec.ts @@ -26,7 +26,7 @@ test('can create content with the upload vector graphics data type', async ({umb // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVideo.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVideo.spec.ts index e109e9b970..da8ea468e7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVideo.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithUploadVideo.spec.ts @@ -26,7 +26,7 @@ test('can create content with the upload video data type', async ({umbracoApi, u // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts index 8a22f75359..28370e69ea 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts @@ -35,7 +35,7 @@ test.afterEach(async ({umbracoApi}) => { test('can add a culture', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCultureAndHostnamesButton(); + await umbracoUi.content.clickCultureAndHostnamesActionMenuOption(); await umbracoUi.content.selectCultureLanguageOption(languageName); await umbracoUi.content.clickSaveModalButton(); @@ -50,7 +50,7 @@ test('can add a culture', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { test('can add a domain', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCultureAndHostnamesButton(); + await umbracoUi.content.clickCultureAndHostnamesActionMenuOption(); await umbracoUi.content.clickAddNewDomainButton(); await umbracoUi.waitForTimeout(500); await umbracoUi.content.enterDomain(domainName); @@ -78,7 +78,7 @@ test('can update culture and hostname', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCultureAndHostnamesButton(); + await umbracoUi.content.clickCultureAndHostnamesActionMenuOption(); await umbracoUi.content.enterDomain(updatedDomainName); await umbracoUi.content.clickSaveModalButton(); @@ -98,7 +98,7 @@ test('can delete culture and hostname', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCultureAndHostnamesButton(); + await umbracoUi.content.clickCultureAndHostnamesActionMenuOption(); await umbracoUi.content.clickDeleteDomainButton(); await umbracoUi.content.clickSaveModalButton(); @@ -119,7 +119,7 @@ test('can add culture and hostname for multiple languages', async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickCultureAndHostnamesButton(); + await umbracoUi.content.clickCultureAndHostnamesActionMenuOption(); await umbracoUi.content.clickAddNewDomainButton(); await umbracoUi.content.enterDomain(domainName, 0); await umbracoUi.content.selectDomainLanguageOption(languageName, 0); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts index c23649c9c4..197a882e4f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts @@ -19,7 +19,7 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => { // Publish the content await umbracoUi.content.goToSection(ConstantHelper.sections.content); await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickPublishButton(); + await umbracoUi.content.clickPublishActionMenuOption(); await umbracoUi.content.clickConfirmToPublishButton(); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts index d08ca78c3b..3d3b09d7ef 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts @@ -1,46 +1,33 @@ -import { - ConstantHelper, - NotificationConstantHelper, - test, -} from "@umbraco/playwright-testhelpers"; -import { expect } from "@playwright/test"; +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; -const contentName = "TestContent"; -const documentTypeName = "TestDocumentTypeForContent"; -const customDataTypeName = "Test RTE Tiptap"; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const customDataTypeName = 'Test RTE Tiptap'; let customDataTypeId = null; -test.beforeEach(async ({ umbracoApi }) => { - customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType( - customDataTypeName - ); +test.beforeEach(async ({umbracoApi}) => { + customDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customDataTypeName); await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.document.ensureNameNotExists(contentName); }); -test.afterEach(async ({ umbracoApi }) => { +test.afterEach(async ({umbracoApi}) => { await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.documentType.ensureNameNotExists(documentTypeName); await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); }); -test("can create content with empty RTE Tiptap property editor", async ({ - umbracoApi, - umbracoUi, -}) => { +test('can create content with empty RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const expectedState = "Draft"; - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor( - documentTypeName, - customDataTypeName, - customDataTypeId - ); + const expectedState = 'Draft'; + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); @@ -54,24 +41,17 @@ test("can create content with empty RTE Tiptap property editor", async ({ expect(contentData.values).toEqual([]); }); -test("can create content with non-empty RTE Tiptap property editor", async ({ - umbracoApi, - umbracoUi, -}) => { +test('can create content with non-empty RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const expectedState = "Draft"; - const inputText = "Test Tiptap here"; - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor( - documentTypeName, - customDataTypeName, - customDataTypeId - ); + const expectedState = 'Draft'; + const inputText = 'Test Tiptap here'; + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.enterRTETipTapEditor(inputText); @@ -83,24 +63,14 @@ test("can create content with non-empty RTE Tiptap property editor", async ({ expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); - expect(contentData.values[0].value.markup).toEqual( - "

" + inputText + "

" - ); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); }); -test("can publish content with RTE Tiptap property editor", async ({ - umbracoApi, - umbracoUi, -}) => { +test('can publish content with RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const expectedState = "Published"; - const inputText = "Test Tiptap here"; - const documentTypeId = - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor( - documentTypeName, - customDataTypeName, - customDataTypeId - ); + const expectedState = 'Published'; + const inputText = 'Test Tiptap here'; + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -116,67 +86,46 @@ test("can publish content with RTE Tiptap property editor", async ({ expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); - expect(contentData.values[0].value.markup).toEqual( - "

" + inputText + "

" - ); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); }); -test.fixme( - "can add a media in RTE Tiptap property editor", - async ({ umbracoApi, umbracoUi }) => { - // Arrange - const iconTitle = "Media Picker"; - const imageName = "Test Image For Content"; - await umbracoApi.media.ensureNameNotExists(imageName); - await umbracoApi.media.createDefaultMediaWithImage(imageName); - const documentTypeId = - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor( - documentTypeName, - customDataTypeName, - customDataTypeId - ); - await umbracoApi.document.createDefaultDocument( - contentName, - documentTypeId - ); - await umbracoUi.goToBackOffice(); - await umbracoUi.content.goToSection(ConstantHelper.sections.content); - - // 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(); - await umbracoUi.content.clickSaveButton(); - - // Assert - //await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); - await umbracoUi.content.isErrorNotificationVisible(false); - expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); - const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value.markup).toContain(" { +test.fixme('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const iconTitle = "Embed"; - const videoURL = "https://www.youtube.com/watch?v=Yu29dE-0OoI"; - const documentTypeId = - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor( - documentTypeName, - customDataTypeName, - customDataTypeId - ); + const iconTitle = 'Media Picker'; + const imageName = 'Test Image For Content'; + await umbracoApi.media.ensureNameNotExists(imageName); + await umbracoApi.media.createDefaultMediaWithImage(imageName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // 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(); + await umbracoUi.content.clickSaveButton(); + + // Assert + //await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.isErrorNotificationVisible(false); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toContain(' { + // Arrange + const iconTitle = 'Embed'; + const videoURL = 'https://www.youtube.com/watch?v=Yu29dE-0OoI'; + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -195,22 +144,14 @@ test("can add a video in RTE Tiptap property editor", async ({ await umbracoUi.content.isErrorNotificationVisible(false); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); - expect(contentData.values[0].value.markup).toContain("data-embed-url"); + expect(contentData.values[0].value.markup).toContain('data-embed-url'); expect(contentData.values[0].value.markup).toContain(videoURL); }); -test("cannot submit an empty link in RTE Tiptap property editor", async ({ - umbracoApi, - umbracoUi, -}) => { +test('cannot submit an empty link in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const iconTitle = "Link"; - const documentTypeId = - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor( - documentTypeName, - customDataTypeName, - customDataTypeId - ); + const iconTitle = 'Link'; + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -219,31 +160,21 @@ test("cannot submit an empty link in RTE Tiptap property editor", async ({ await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); await umbracoUi.content.clickManualLinkButton(); - await umbracoUi.content.enterLink(""); - await umbracoUi.content.enterAnchorOrQuerystring(""); - await umbracoUi.content.enterLinkTitle(""); + await umbracoUi.content.enterLink(''); + await umbracoUi.content.enterAnchorOrQuerystring(''); + await umbracoUi.content.enterLinkTitle(''); await umbracoUi.content.clickAddButton(); // Assert - await umbracoUi.content.isTextWithMessageVisible( - ConstantHelper.validationMessages.emptyLinkPicker - ); + await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); }); // TODO: Remove skip when the front-end ready. Currently it still accept the empty link with an anchor or querystring // Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17411 -test.skip("cannot submit an empty URL with an anchor or querystring in RTE Tiptap property editor", async ({ - umbracoApi, - umbracoUi, -}) => { +test.skip('cannot submit an empty URL with an anchor or querystring in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const iconTitle = "Link"; - const documentTypeId = - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor( - documentTypeName, - customDataTypeName, - customDataTypeId - ); + const iconTitle = 'Link'; + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -252,42 +183,26 @@ test.skip("cannot submit an empty URL with an anchor or querystring in RTE Tipta await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); await umbracoUi.content.clickManualLinkButton(); - await umbracoUi.content.enterLink(""); - await umbracoUi.content.enterAnchorOrQuerystring("#value"); + await umbracoUi.content.enterLink(''); + await umbracoUi.content.enterAnchorOrQuerystring('#value'); await umbracoUi.content.clickAddButton(); // Assert - await umbracoUi.content.isTextWithMessageVisible( - ConstantHelper.validationMessages.emptyLinkPicker - ); + await umbracoUi.content.isTextWithMessageVisible(ConstantHelper.validationMessages.emptyLinkPicker); }); // TODO: Remove skip when the front-end ready. Currently it is impossible to link to unpublished document // Issue link: https://github.com/umbraco/Umbraco-CMS/issues/17974 -test.skip("can insert a link to an unpublished document in RTE Tiptap property editor", async ({ - umbracoApi, - umbracoUi, -}) => { +test.skip('can insert a link to an unpublished document in RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const iconTitle = "Link"; - const documentTypeId = - await umbracoApi.documentType.createDocumentTypeWithPropertyEditor( - documentTypeName, - customDataTypeName, - customDataTypeId - ); + const iconTitle = 'Link'; + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); // Create a document to link - const documentTypeForLinkedDocumentName = "TestDocumentType"; - const documentTypeForLinkedDocumentId = - await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot( - documentTypeForLinkedDocumentName - ); - const linkedDocumentName = "LinkedDocument"; - await umbracoApi.document.createDefaultDocument( - linkedDocumentName, - documentTypeForLinkedDocumentId - ); + const documentTypeForLinkedDocumentName = 'TestDocumentType'; + const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); + const linkedDocumentName = 'LinkedDocument'; + await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -296,7 +211,7 @@ test.skip("can insert a link to an unpublished document in RTE Tiptap property e await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); await umbracoUi.content.clickDocumentLinkButton(); await umbracoUi.content.selectLinkByName(linkedDocumentName); - await umbracoUi.content.clickButtonWithName("Choose"); + await umbracoUi.content.clickButtonWithName('Choose'); await umbracoUi.content.clickAddButton(); await umbracoUi.content.clickSaveButton(); @@ -305,8 +220,6 @@ test.skip("can insert a link to an unpublished document in RTE Tiptap property e await umbracoUi.content.isErrorNotificationVisible(false); // Clean - await umbracoApi.documentType.ensureNameNotExists( - documentTypeForLinkedDocumentName - ); + await umbracoApi.documentType.ensureNameNotExists(documentTypeForLinkedDocumentName); await umbracoApi.document.ensureNameNotExists(linkedDocumentName); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/SecondLevelBlockProperties.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/SecondLevelBlockProperties.spec.ts new file mode 100644 index 0000000000..63179d576a --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/SecondLevelBlockProperties.spec.ts @@ -0,0 +1,165 @@ +import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +// Content Name +const contentName = 'ContentName'; + +// Document Type +const documentTypeName = 'DocumentTypeName'; +let documentTypeId = null; +const documentTypeGroupName = 'DocumentGroup'; + +// Rich Text Editor +const richTextDataTypeName = 'RichTextDataType'; +let richTextDataTypeId = null; + +// Text String +const textStringElementTypeName = 'TextStringElementName'; +let textStringElementTypeId = null; +let textStringGroupName = 'TextGroup'; +const textStringDataTypeName = 'Textstring'; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(richTextDataTypeName); +}); + +test('can publish a rich text editor with a rich text editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const richTextEditorValue = 'Hello First World'; + const secondRichTextEditorValue = 'Hello Second World'; + const expectedRichTextEditorOutputValue = '

' + richTextEditorValue + '

'; + const secondExpectedRichTextEditorOutputValue = '

' + secondRichTextEditorValue + '

'; + const secondRichTextDataTypeName = 'SecondRichTextName'; + const richTextElementTypeName = 'RichTextElementName'; + const richTextElementGroupName = 'RichTextElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(secondRichTextDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(richTextElementGroupName); + + const secondRichTextEditorDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(secondRichTextDataTypeName); + const richTextElementTypeId = await umbracoApi.documentType.createDefaultElementType(richTextElementTypeName, richTextElementGroupName, secondRichTextDataTypeName, secondRichTextEditorDataTypeId); + richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, richTextElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.enterRTETipTapEditor(richTextEditorValue); + await umbracoUi.content.clickInsertBlockButton(); + await umbracoUi.content.clickBlockCardWithName(richTextElementTypeName, true); + await umbracoUi.content.enterRTETipTapEditorWithName(AliasHelper.toAlias(secondRichTextDataTypeName), secondRichTextEditorValue); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the RTE is as expected + const documentData = await umbracoApi.document.getByName(contentName); + const documentValues = documentData.values.find(value => value.alias === AliasHelper.toAlias(richTextDataTypeName)); + // Value in the first RTE + expect(documentValues.value.markup).toContain(expectedRichTextEditorOutputValue); + // Value in the second RTE + const secondRTEInBlock = documentValues.value.blocks.contentData[0].values.find(value => value.alias === AliasHelper.toAlias(secondRichTextDataTypeName)); + expect(secondRTEInBlock.value.markup).toContain(secondExpectedRichTextEditorOutputValue); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(richTextElementGroupName); +}); + +test('can publish a rich text editor with a block grid editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const blockGridDataTypeName = 'BlockGridDataTypeName'; + const blockGridElementTypeName = 'BlockGridElementTypeName'; + const blockGridElementGroupName = 'BlockGridElementGroupName'; + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithABlockWithInlineEditingMode(blockGridDataTypeName, textStringElementTypeId, true); + const blockGridElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockGridElementTypeName, blockGridElementGroupName, blockGridDataTypeName, blockGridDataTypeId); + richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockGridElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickInsertBlockButton(); + await umbracoUi.content.clickBlockCardWithName(blockGridElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockGrid is as expected + const documentData = await umbracoApi.document.getByName(contentName); + expect(documentData.values[0].value.blocks.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockGridElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); +}); + +test('can publish a rich text editor with a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringValue = 'Hello World'; + const blockListDataTypeName = 'BlockListName'; + const blockListElementTypeName = 'BlockListElementName'; + const blockListElementGroupName = 'BlockListGroupName'; + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); + + const textStringDataType = await umbracoApi.dataType.getByName(textStringDataTypeName); + textStringElementTypeId = await umbracoApi.documentType.createDefaultElementType(textStringElementTypeName, textStringGroupName, textStringDataTypeName, textStringDataType.id); + const blockListDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListDataTypeName, textStringElementTypeId); + const blockListElementTypeId = await umbracoApi.documentType.createDefaultElementType(blockListElementTypeName, blockListElementGroupName, blockListDataTypeName, blockListDataTypeId); + richTextDataTypeId = await umbracoApi.dataType.createRichTextEditorWithABlock(richTextDataTypeName, blockListElementTypeId); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, richTextDataTypeName, richTextDataTypeId, documentTypeGroupName); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickInsertBlockButton(); + await umbracoUi.content.clickBlockCardWithName(blockListElementTypeName, true); + await umbracoUi.content.clickAddBlockWithNameButton(textStringElementTypeName); + await umbracoUi.content.clickBlockCardWithName(textStringElementTypeName, true); + await umbracoUi.content.enterTextstring(textStringValue); + await umbracoUi.content.clickCreateForModalWithHeadline('Add ' + textStringElementTypeName); + await umbracoUi.content.clickCreateModalButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + // Asserts that the value in the BlockGrid is as expected + const documentData = await umbracoApi.document.getByName(contentName); + expect(documentData.values[0].value.blocks.contentData[0].values[0].value.contentData[0].values[0].value).toContain(textStringValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(blockListDataTypeName); + await umbracoApi.documentType.ensureNameNotExists(blockListElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(textStringElementTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapStyleSelect.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapStyleSelect.spec.ts new file mode 100644 index 0000000000..faab11fe5f --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/TiptapStyleSelect.spec.ts @@ -0,0 +1,114 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const customDataTypeName = 'Test RTE Tiptap Style Select'; +const inputText = 'This is Tiptap test'; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + const customDataTypeId = await umbracoApi.dataType.createTiptapDataTypeWithStyleSelect(customDataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.enterRTETipTapEditor(inputText); + await umbracoUi.content.selectAllRTETipTapEditorText(); +}) + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can apply page header format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Headers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Page header'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply section header format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Headers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Section header'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply paragraph header format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Headers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Paragraph header'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply paragraph blocks format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Blocks'); + await umbracoUi.content.clickCascadingMenuItemWithName('Paragraph'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply block quote format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Containers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Block quote'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('

' + inputText + '

'); +}); + +test('can apply code block format', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.content.clickStyleSelectButton(); + + // Act + await umbracoUi.content.hoverCascadingMenuItemWithName('Containers'); + await umbracoUi.content.clickCascadingMenuItemWithName('Code block'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].value.markup).toEqual('
' + inputText + '

'); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts index 45e70f3281..40f2e64ad9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts @@ -80,7 +80,8 @@ test('can not create unsupported invariant document type with invariant tiptap R await umbracoUi.content.clickSaveAndPublishButton(); // Assert - await umbracoUi.content.isErrorNotificationVisible(); + //await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); + await umbracoUi.content.isErrorNotificationVisible(false); await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts index 4214ff691a..0d3c854141 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent/TrashContent.spec.ts @@ -31,7 +31,7 @@ test('can trash an invariant content node', {tag: '@smoke'}, async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); // Verify the references list not displayed await umbracoUi.content.isReferenceHeadlineVisible(false); await umbracoUi.content.clickConfirmTrashButton(); @@ -52,7 +52,7 @@ test('can trash a variant content node', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); // Verify the references list not displayed await umbracoUi.content.isReferenceHeadlineVisible(false); await umbracoUi.content.clickConfirmTrashButton(); @@ -74,7 +74,7 @@ test('can trash a published content node', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); // Verify the references list not displayed await umbracoUi.content.isReferenceHeadlineVisible(false); await umbracoUi.content.clickConfirmTrashButton(); @@ -99,7 +99,7 @@ test('can trash an invariant content node that references one item', async ({umb // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); // Verify the references list await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); await umbracoUi.content.doesReferenceItemsHaveCount(1); @@ -126,7 +126,7 @@ test('can trash a variant content node that references one item', async ({umbrac // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); // Verify the references list await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); await umbracoUi.content.doesReferenceItemsHaveCount(1); @@ -159,7 +159,7 @@ test('can trash an invariant content node that references more than 3 items', as // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); // Verify the references list has 3 items and has the text '...and one more item' await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); await umbracoUi.content.doesReferenceItemsHaveCount(3); @@ -200,7 +200,7 @@ test('can trash a variant content node that references more than 3 items', async // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); // Verify the references list has 3 items and has the text '...and one more item' await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); await umbracoUi.content.doesReferenceItemsHaveCount(3); @@ -239,7 +239,7 @@ test('can trash a content node with multiple cultures that references one item', // Act await umbracoUi.content.clickActionsMenuForContent(contentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); // Verify the references list await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); await umbracoUi.content.doesReferenceItemsHaveCount(1); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts index 97f8d8303c..7b55e7f3ba 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts @@ -24,7 +24,7 @@ test('can create a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbr // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickCreateActionMenuOption(); await umbracoUi.dataType.clickDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(blockGridEditorName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); @@ -213,7 +213,7 @@ test('can remove a block in a group from a block grid editor', {tag: '@smoke'}, expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockGridEditorName, [elementTypeId])).toBeFalsy(); }); -test('can move a block from a group to another group in a block grid editor', async ({umbracoApi, umbracoUi}) => { +test.fixme('can move a block from a group to another group in a block grid editor', async ({umbracoApi, umbracoUi}) => { // Arrange const textStringData = await umbracoApi.dataType.getByName(dataTypeName); const secondGroupName = 'MoveToHereGroup'; @@ -228,6 +228,7 @@ test('can move a block from a group to another group in a block grid editor', as // Drag and Drop const dragFromLocator = await umbracoUi.dataType.getLinkWithName(elementTypeName); const dragToLocator = await umbracoUi.dataType.getAddButtonInGroupWithName(secondGroupName); + // This needs to be fixed await umbracoUi.dataType.dragAndDrop(dragFromLocator, dragToLocator, -10, 0, 10); await umbracoUi.dataType.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts index e41a8da3bd..874974ab9e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts @@ -24,7 +24,7 @@ test('can create a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbr // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickCreateActionMenuOption(); await umbracoUi.dataType.clickDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(blockListEditorName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); @@ -64,7 +64,7 @@ test('can delete a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbr // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickActionsMenuForDataType(blockListEditorName); - await umbracoUi.dataType.clickDeleteButton(); + await umbracoUi.dataType.clickDeleteActionMenuOption(); await umbracoUi.dataType.clickConfirmToDeleteButton(); // 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 7b1229a0c9..5cc6841c5b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts @@ -16,7 +16,7 @@ test.afterEach(async ({umbracoApi}) => { test('can create a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickCreateActionMenuOption(); await umbracoUi.dataType.clickDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); @@ -94,7 +94,7 @@ test('can change property editor in a data type', {tag: '@smoke'}, async ({umbra test('cannot create a data type without selecting the property editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickCreateActionMenuOption(); await umbracoUi.dataType.clickDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeCollectionWorkspace.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeCollectionWorkspace.spec.ts index 8e68c768ce..68f919dc61 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeCollectionWorkspace.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeCollectionWorkspace.spec.ts @@ -21,7 +21,7 @@ test('can create a data type using create options', async ({umbracoApi, umbracoU await umbracoUi.dataType.clickDataTypesMenu(); // Act - await umbracoUi.dataType.clickCreateActionWithOptionName('Data Type'); + await umbracoUi.dataType.clickCreateActionWithOptionName('New Data Type'); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); await umbracoUi.dataType.selectAPropertyEditor('Text Box'); @@ -62,7 +62,7 @@ test('can create a data type in a folder using create options', async ({umbracoA await umbracoUi.dataType.goToDataType(dataTypeFolderName); // Act - await umbracoUi.dataType.clickCreateActionWithOptionName('Data Type'); + await umbracoUi.dataType.clickCreateActionWithOptionName('New Data Type'); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); await umbracoUi.dataType.selectAPropertyEditor('Text Box'); 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 b9c28c70ac..95a8c5fc87 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts @@ -38,7 +38,7 @@ test('can rename a data type folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickActionsMenuForDataType(wrongDataTypeFolderName); - await umbracoUi.dataType.clickRenameFolderButton(); + await umbracoUi.dataType.clickRenameActionMenuOption(); await umbracoUi.dataType.enterFolderName(dataTypeFolderName); await umbracoUi.dataType.clickConfirmRenameButton(); @@ -73,7 +73,7 @@ test('can create a data type in a folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickActionsMenuForDataType(dataTypeFolderName); - await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickCreateActionMenuOption(); await umbracoUi.dataType.clickDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); 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 f502a878d0..e61aa1b40e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts @@ -21,7 +21,7 @@ test('can create a rich text editor with tiptap', {tag: '@smoke'}, async ({umbra // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickCreateActionMenuOption(); await umbracoUi.dataType.clickDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(tipTapName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts index c311419f2d..0bfee39f91 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts @@ -64,7 +64,7 @@ test('can create a dictionary item in a dictionary', {tag: '@smoke'}, async ({um // Act await umbracoUi.dictionary.clickActionsMenuForDictionary(parentDictionaryName); - await umbracoUi.dictionary.clickCreateButton(); + await umbracoUi.dictionary.clickCreateActionMenuOption(); await umbracoUi.dictionary.enterDictionaryName(dictionaryName); await umbracoUi.dictionary.clickSaveButton(); @@ -92,7 +92,7 @@ test('can export a dictionary item', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.dictionary.clickExportButton(); + await umbracoUi.dictionary.clickExportActionMenuOption(); const exportData = await umbracoUi.dictionary.exportDictionary(false); // Assert @@ -108,7 +108,7 @@ test('can export a dictionary item with descendants', {tag: '@smoke'}, async ({u // Act await umbracoUi.dictionary.clickActionsMenuForDictionary(parentDictionaryName); - await umbracoUi.dictionary.clickExportButton(); + await umbracoUi.dictionary.clickExportActionMenuOption(); const exportData = await umbracoUi.dictionary.exportDictionary(true); // Assert @@ -133,6 +133,7 @@ test('can import a dictionary item', async ({umbracoApi, umbracoUi}) => { await umbracoUi.dictionary.importDictionary(udtFilePath); // Assert + await umbracoUi.waitForTimeout(500); // Verify the imported dictionary item displays in the list await umbracoUi.reloadPage(); expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importDictionaryName)).toBeTruthy(); @@ -153,7 +154,7 @@ test('can import a dictionary item with descendants', {tag: '@smoke'}, async ({u // Act await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.dictionary.clickImportButton(); + await umbracoUi.dictionary.clickImportActionMenuOption(); await umbracoUi.dictionary.importDictionary(udtFilePath); // These timeouts are necessary as this test can fail await umbracoUi.waitForTimeout(500); 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 0bda6d9c64..25c923b50d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -73,7 +73,7 @@ for (const mediaFileType of mediaFileTypes) { // Assert //await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); - await umbracoUi.media.isErrorNotificationVisible(false); + await umbracoUi.media.isErrorNotificationVisible(false); const mediaData = await umbracoApi.media.getByName(mediaFileType.fileName); const mediaUrl = await umbracoApi.media.getMediaUrl(mediaData.id); await umbracoUi.media.doesMediaHaveThumbnail(mediaData.id, mediaFileType.thumbnail, mediaUrl); @@ -115,7 +115,7 @@ test('can trash a folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.media.clickActionsMenuForName(folderName); - await umbracoUi.media.clickTrashButton(); + await umbracoUi.media.clickTrashActionMenuOption(); await umbracoUi.media.clickConfirmTrashButton(); // Assert @@ -133,7 +133,7 @@ test('can create a folder in a folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.media.clickActionsMenuForName(parentFolderName); - await umbracoUi.media.clickCreateModalButton(); + await umbracoUi.media.clickCreateActionMenuOption(); await umbracoUi.media.clickMediaTypeName('Folder'); await umbracoUi.media.enterMediaItemName(folderName); await umbracoUi.media.clickSaveButton(); @@ -178,9 +178,7 @@ test('can trash a media item', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.media.clickActionsMenuForName(mediaFileName); - await umbracoUi.media.clickTrashButton(); - // Verify the references list not displayed - await umbracoUi.content.isReferenceHeadlineVisible(false); + await umbracoUi.media.clickTrashActionMenuOption(); await umbracoUi.media.clickConfirmTrashButton(); // Assert @@ -274,7 +272,8 @@ test('can trash a media node with a relation', async ({umbracoApi, umbracoUi}) = await umbracoUi.media.clickConfirmTrashButton(); // Assert - await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + // await umbracoUi.media.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + await umbracoUi.media.isErrorNotificationVisible(false); await umbracoUi.media.isItemVisibleInRecycleBin(mediaFileName); expect(await umbracoApi.media.doesNameExist(mediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(mediaFileName)).toBeTruthy(); @@ -312,7 +311,8 @@ test('can bulk trash media nodes with a relation', async ({umbracoApi, umbracoUi await umbracoUi.media.clickConfirmTrashButton(); // Assert - await umbracoUi.media.isSuccessNotificationVisible(); + // await umbracoUi.media.isSuccessNotificationVisible(); + await umbracoUi.media.isErrorNotificationVisible(false); expect(await umbracoApi.media.doesNameExist(firstMediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesNameExist(secondMediaFileName)).toBeFalsy(); expect(await umbracoApi.media.doesMediaItemExistInRecycleBin(firstMediaFileName)).toBeTruthy(); @@ -328,4 +328,4 @@ test('can bulk trash media nodes with a relation', async ({umbracoApi, umbracoUi await umbracoApi.document.ensureNameNotExists(documentPickerName2[0]); await umbracoApi.documentType.ensureNameNotExists(documentPickerName2[1]); await umbracoApi.media.emptyRecycleBin(); -}); \ No newline at end of file +}); 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 a36e52b530..e20c444ced 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/Members.spec.ts @@ -27,7 +27,7 @@ test('can create a member', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => await umbracoUi.member.goToMembers(); // Act - await umbracoUi.member.clickCreateButton(); + await umbracoUi.member.clickCreateMembersButton(); await umbracoUi.member.enterMemberName(memberName); await umbracoUi.member.enterComments(comment); await umbracoUi.member.clickInfoTab(); @@ -233,7 +233,7 @@ test('cannot create member with invalid email', async ({umbracoApi, umbracoUi}) await umbracoUi.member.goToMembers(); // Act - await umbracoUi.member.clickCreateButton(); + await umbracoUi.member.clickCreateMembersButton(); await umbracoUi.member.enterMemberName(memberName); await umbracoUi.member.enterComments(comment); await umbracoUi.member.clickInfoTab(); 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 15bfcb710b..f831ca285e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RelationTypes/RelationTypes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RelationTypes/RelationTypes.spec.ts @@ -21,7 +21,7 @@ test.afterEach(async ({umbracoApi}) => { test.skip('can create a relation type', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.relationType.clickActionsMenuAtRoot(); - await umbracoUi.relationType.clickCreateButton(); + await umbracoUi.relationType.clickCreateActionMenuOption(); await umbracoUi.relationType.enterRelationTypeName(relationTypeName); await umbracoUi.relationType.selectParentOption(objectTypeName); await umbracoUi.relationType.selectChildOption(objectTypeName); @@ -93,7 +93,7 @@ test.skip('can delete a relation type', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.relationType.clickRootFolderCaretButton(); await umbracoUi.relationType.clickActionsMenuForRelationType(relationTypeName); - await umbracoUi.relationType.clickDeleteButton(); + await umbracoUi.relationType.clickDeleteActionMenuOption(); await umbracoUi.relationType.clickConfirmToDeleteButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts index ace79eaf62..09db87d8a8 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts @@ -1,4 +1,4 @@ -import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const documentBlueprintName = 'TestDocumentBlueprints'; @@ -23,7 +23,7 @@ test('can create a document blueprint from the settings menu', {tag: '@smoke'}, // Act await umbracoUi.documentBlueprint.clickActionsMenuAtRoot(); - await umbracoUi.documentBlueprint.clickCreateDocumentBlueprintButton(); + await umbracoUi.documentBlueprint.clickCreateActionMenuOption(); await umbracoUi.documentBlueprint.clickTextButtonWithName(documentTypeName); await umbracoUi.documentBlueprint.enterDocumentBlueprintName(documentBlueprintName); await umbracoUi.documentBlueprint.clickSaveButton(); @@ -66,7 +66,7 @@ test('can delete a document blueprint', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.documentBlueprint.reloadDocumentBlueprintsTree(); await umbracoUi.documentBlueprint.clickActionsMenuForDocumentBlueprints(documentBlueprintName); - await umbracoUi.documentBlueprint.clickDeleteMenuButton(); + await umbracoUi.documentBlueprint.clickDeleteActionMenuOption(); await umbracoUi.documentBlueprint.clickConfirmToDeleteButton(); // Assert @@ -85,7 +85,7 @@ test('can create a document blueprint from the content menu', async ({umbracoApi // Act await umbracoUi.content.clickActionsMenuForContent(documentBlueprintName); - await umbracoUi.content.clickCreateDocumentBlueprintButton(); + await umbracoUi.content.clickCreateBlueprintActionMenuOption(); await umbracoUi.content.clickSaveModalButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts index 462eaba4d9..0825634436 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts @@ -18,7 +18,7 @@ test('can create a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoU // Act await umbracoUi.documentType.clickActionsMenuAtRoot(); - await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateActionMenuOption(); await umbracoUi.documentType.clickCreateDocumentTypeButton(); await umbracoUi.documentType.enterDocumentTypeName(documentTypeName); await umbracoUi.documentType.clickSaveButton(); @@ -38,7 +38,7 @@ test('can create a document type with a template', {tag: '@smoke'}, async ({umbr // Act await umbracoUi.documentType.clickActionsMenuAtRoot(); - await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateActionMenuOption(); await umbracoUi.documentType.clickCreateDocumentTypeWithTemplateButton(); await umbracoUi.documentType.enterDocumentTypeName(documentTypeName); await umbracoUi.documentType.clickSaveButton(); @@ -63,7 +63,7 @@ test('can create a element type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi // Act await umbracoUi.documentType.clickActionsMenuAtRoot(); - await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateActionMenuOption(); await umbracoUi.documentType.clickCreateElementTypeButton(); await umbracoUi.documentType.enterDocumentTypeName(documentTypeName); await umbracoUi.documentType.clickSaveButton(); 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 26d8c5fd6a..031b8fa364 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 @@ -224,7 +224,7 @@ test('can create a document type with a composition', {tag: '@smoke'}, async ({u await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.waitForTimeout(500); await umbracoUi.documentType.clickCompositionsButton(); - await umbracoUi.documentType.clickButtonWithName(compositionDocumentTypeName); + await umbracoUi.documentType.clickModalMenuItemWithName(compositionDocumentTypeName); await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickSaveButton(); @@ -253,7 +253,7 @@ test('can remove a composition from a document type', async ({umbracoApi, umbrac await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.waitForTimeout(500); await umbracoUi.documentType.clickCompositionsButton(); - await umbracoUi.documentType.clickButtonWithName(compositionDocumentTypeName); + await umbracoUi.documentType.clickModalMenuItemWithName(compositionDocumentTypeName); await umbracoUi.documentType.clickSubmitButton(); await umbracoUi.documentType.clickConfirmToSubmitButton(); 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 c7b2607aa6..e956c58e8f 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 @@ -15,8 +15,8 @@ test.afterEach(async ({umbracoApi}) => { test('can create a empty document type folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); - await umbracoUi.documentType.clickActionsMenuForName('Document Types'); - await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickActionsMenuAtRoot(); + await umbracoUi.documentType.clickCreateActionMenuOption(); await umbracoUi.documentType.clickCreateDocumentFolderButton(); await umbracoUi.documentType.enterFolderName(documentFolderName); await umbracoUi.documentType.clickCreateFolderButton(); @@ -39,7 +39,7 @@ test('can delete a document type folder', {tag: '@smoke'}, async ({umbracoApi, u await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); await umbracoUi.documentType.clickRootFolderCaretButton(); await umbracoUi.documentType.clickActionsMenuForName(documentFolderName); - await umbracoUi.documentType.deleteFolder(); + await umbracoUi.documentType.clickDeleteAndConfirmButton(); // Assert //await umbracoUi.documentType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.deleted); @@ -58,9 +58,10 @@ test('can rename a document type folder', async ({umbracoApi, umbracoUi}) => { await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); await umbracoUi.documentType.clickRootFolderCaretButton(); await umbracoUi.documentType.clickActionsMenuForName(oldFolderName); - await umbracoUi.documentType.clickRenameFolderButton(); + await umbracoUi.documentType.clickRenameActionMenuOption(); await umbracoUi.documentType.enterFolderName(documentFolderName); await umbracoUi.documentType.clickConfirmRenameButton(); + await umbracoUi.waitForTimeout(500); // Assert //await umbracoUi.documentType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved); @@ -81,7 +82,7 @@ test('can create a document type folder in a folder', async ({umbracoApi, umbrac await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); await umbracoUi.documentType.clickRootFolderCaretButton(); await umbracoUi.documentType.clickActionsMenuForName(documentFolderName); - await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateActionMenuOption(); await umbracoUi.documentType.clickCreateDocumentFolderButton(); await umbracoUi.documentType.enterFolderName(childFolderName); await umbracoUi.documentType.clickCreateFolderButton(); @@ -113,7 +114,7 @@ test('can create a folder in a folder in a folder', {tag: '@smoke'}, async ({umb await umbracoUi.documentType.clickRootFolderCaretButton(); await umbracoUi.documentType.clickCaretButtonForName(grandParentFolderName); await umbracoUi.documentType.clickActionsMenuForName(parentFolderName); - await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateActionMenuOption(); await umbracoUi.documentType.clickCreateDocumentFolderButton(); await umbracoUi.documentType.enterFolderName(documentFolderName); await umbracoUi.documentType.clickCreateFolderButton(); 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 450d78e7c0..0a74181e39 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 @@ -39,7 +39,7 @@ test('can add an allowed child node to a document type', {tag: '@smoke'}, async await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickStructureTab(); await umbracoUi.documentType.clickChooseButton(); - await umbracoUi.documentType.clickButtonWithName(documentTypeName); + await umbracoUi.documentType.clickModalMenuItemWithName(documentTypeName); await umbracoUi.documentType.clickAllowedChildNodesButton(); await umbracoUi.documentType.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts index 8ef912adbd..9a187be235 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts @@ -20,7 +20,7 @@ test('can add language', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { await umbracoUi.language.goToLanguages(); // Act - await umbracoUi.language.clickCreateLink(); + await umbracoUi.language.clickLanguageCreateButton(); await umbracoUi.language.chooseLanguageByName(languageName); await umbracoUi.language.clickSaveButton(); @@ -135,7 +135,7 @@ test('cannot add a language with duplicate ISO code', async ({umbracoApi, umbrac await umbracoUi.language.goToLanguages(); // Act - await umbracoUi.language.clickCreateLink(); + await umbracoUi.language.clickLanguageCreateButton(); await umbracoUi.language.chooseLanguageByName(languageName); await umbracoUi.language.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaType.spec.ts index a617b95ae4..3849bae287 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaType.spec.ts @@ -15,8 +15,8 @@ test.afterEach(async ({umbracoApi}) => { test('can create a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act - await umbracoUi.mediaType.clickActionsMenuForName('Media Types'); - await umbracoUi.mediaType.clickCreateButton(); + await umbracoUi.mediaType.clickActionsMenuAtRoot(); + await umbracoUi.mediaType.clickCreateActionMenuOption(); await umbracoUi.mediaType.clickMediaTypeButton(); await umbracoUi.mediaType.enterMediaTypeName(mediaTypeName); await umbracoUi.mediaType.clickSaveButton(); @@ -90,7 +90,7 @@ test('can delete a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) // Act await umbracoUi.mediaType.clickRootFolderCaretButton(); await umbracoUi.mediaType.clickActionsMenuForName(mediaTypeName); - await umbracoUi.mediaType.clickDeleteButton(); + await umbracoUi.mediaType.clickDeleteActionMenuOption(); await umbracoUi.mediaType.clickConfirmToDeleteButton(); // Assert 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 9c63499d57..60d3f7bf6c 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 @@ -294,7 +294,7 @@ test('can create a media type with a composition', async ({umbracoApi, umbracoUi await umbracoUi.mediaType.goToMediaType(mediaTypeName); await umbracoUi.waitForTimeout(500); await umbracoUi.mediaType.clickCompositionsButton(); - await umbracoUi.mediaType.clickButtonWithName(compositionMediaTypeName); + await umbracoUi.mediaType.clickModalMenuItemWithName(compositionMediaTypeName); 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 24906d2ce4..30109d90d8 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 @@ -15,8 +15,8 @@ test.afterEach(async ({umbracoApi}) => { test('can create a empty media type folder', async ({umbracoApi, umbracoUi}) => { // Act - await umbracoUi.mediaType.clickActionsMenuForName('Media Types'); - await umbracoUi.mediaType.clickActionsMenuCreateButton(); + await umbracoUi.mediaType.clickActionsMenuAtRoot(); + await umbracoUi.mediaType.clickCreateActionMenuOption(); await umbracoUi.mediaType.clickFolderButton(); await umbracoUi.mediaType.enterFolderName(mediaTypeFolderName); await umbracoUi.mediaType.clickConfirmCreateFolderButton(); @@ -38,7 +38,7 @@ test('can delete a media type folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.mediaType.clickRootFolderCaretButton(); await umbracoUi.mediaType.clickActionsMenuForName(mediaTypeFolderName); - await umbracoUi.mediaType.deleteFolder(); + await umbracoUi.mediaType.clickDeleteAndConfirmButton(); // Assert //await umbracoUi.mediaType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.deleted); @@ -55,7 +55,7 @@ test('can rename a media type folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.mediaType.clickRootFolderCaretButton(); await umbracoUi.mediaType.clickActionsMenuForName(oldFolderName); - await umbracoUi.mediaType.clickRenameFolderButton(); + await umbracoUi.mediaType.clickUpdateActionMenuOption(); await umbracoUi.waitForTimeout(500); await umbracoUi.mediaType.enterFolderName(mediaTypeFolderName); await umbracoUi.mediaType.clickConfirmRenameButton(); @@ -76,7 +76,7 @@ test('can create a media type folder in a folder', async ({umbracoApi, umbracoUi // Act await umbracoUi.mediaType.clickRootFolderCaretButton(); await umbracoUi.mediaType.clickActionsMenuForName(mediaTypeFolderName); - await umbracoUi.mediaType.clickActionsMenuCreateButton(); + await umbracoUi.mediaType.clickCreateActionMenuOption(); await umbracoUi.mediaType.clickFolderButton(); await umbracoUi.mediaType.enterFolderName(childFolderName); await umbracoUi.mediaType.clickConfirmCreateFolderButton(); @@ -106,7 +106,7 @@ test('can create a media type folder in a folder in a folder', async ({umbracoAp await umbracoUi.mediaType.clickRootFolderCaretButton(); await umbracoUi.mediaType.clickCaretButtonForName(grandparentFolderName); await umbracoUi.mediaType.clickActionsMenuForName(mediaTypeFolderName); - await umbracoUi.mediaType.clickActionsMenuCreateButton(); + await umbracoUi.mediaType.clickCreateActionMenuOption(); await umbracoUi.mediaType.clickFolderButton(); await umbracoUi.mediaType.enterFolderName(childFolderName); await umbracoUi.mediaType.clickConfirmCreateFolderButton(); 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 13d3ac2cfd..5410e830ec 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 @@ -38,7 +38,7 @@ test('can create a media type with an allowed child node type', {tag: '@smoke'}, await umbracoUi.mediaType.goToMediaType(mediaTypeName); await umbracoUi.mediaType.clickStructureTab(); await umbracoUi.mediaType.clickChooseButton(); - await umbracoUi.mediaType.clickButtonWithName(mediaTypeName); + await umbracoUi.mediaType.clickModalMenuItemWithName(mediaTypeName); await umbracoUi.mediaType.clickAllowedChildNodesButton(); await umbracoUi.mediaType.clickSaveButton(); @@ -60,8 +60,8 @@ test('can create a media type with multiple allowed child nodes types', async ({ await umbracoUi.mediaType.goToMediaType(mediaTypeName); await umbracoUi.mediaType.clickStructureTab(); await umbracoUi.mediaType.clickChooseButton(); - await umbracoUi.mediaType.clickButtonWithName(mediaTypeName); - await umbracoUi.mediaType.clickButtonWithName(secondMediaTypeName); + await umbracoUi.mediaType.clickModalMenuItemWithName(mediaTypeName); + await umbracoUi.mediaType.clickModalMenuItemWithName(secondMediaTypeName); await umbracoUi.mediaType.clickAllowedChildNodesButton(); 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 1fbb85f291..47d42fd27f 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 @@ -20,7 +20,7 @@ test.afterEach(async ({umbracoApi}) => { test('can create an empty partial view', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.partialView.clickActionsMenuAtRoot(); - await umbracoUi.partialView.clickCreateButton(); + await umbracoUi.partialView.clickCreateOptionsActionMenuOption(); await umbracoUi.partialView.clickNewEmptyPartialViewButton(); await umbracoUi.partialView.enterPartialViewName(partialViewName); await umbracoUi.partialView.clickSaveButton(); @@ -40,7 +40,7 @@ test('can create a partial view from snippet', async ({umbracoApi, umbracoUi}) = // Act await umbracoUi.partialView.clickActionsMenuAtRoot(); - await umbracoUi.partialView.clickCreateButton(); + await umbracoUi.partialView.clickCreateOptionsActionMenuOption(); await umbracoUi.partialView.clickNewPartialViewFromSnippetButton(); await umbracoUi.partialView.clickBreadcrumbButton(); await umbracoUi.partialView.enterPartialViewName(partialViewName); @@ -290,7 +290,7 @@ test.skip('can show returned items in query builder ', async ({umbracoApi, umbra test('cannot create a partial view with an empty name', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.partialView.clickActionsMenuAtRoot(); - await umbracoUi.partialView.clickCreateButton(); + await umbracoUi.partialView.clickCreateOptionsActionMenuOption(); await umbracoUi.partialView.clickNewEmptyPartialViewButton(); await umbracoUi.partialView.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts index 44e3e8621a..b3d9215036 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialViewFolder.spec.ts @@ -40,7 +40,7 @@ test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => // Act await umbracoUi.partialView.reloadPartialViewTree(); await umbracoUi.partialView.clickActionsMenuForPartialView(folderName); - await umbracoUi.partialView.deleteFolder(); + await umbracoUi.partialView.clickDeleteAndConfirmButton(); // Assert //await umbracoUi.partialView.isSuccessNotificationVisible(); @@ -61,7 +61,7 @@ test('can create a partial view in a folder', async ({umbracoApi, umbracoUi}) => // Act await umbracoUi.partialView.reloadPartialViewTree(); await umbracoUi.partialView.clickActionsMenuForPartialView(folderName); - await umbracoUi.partialView.clickCreateButton(); + await umbracoUi.partialView.clickCreateOptionsActionMenuOption(); await umbracoUi.partialView.clickNewEmptyPartialViewButton(); await umbracoUi.partialView.enterPartialViewName(partialViewName); await umbracoUi.partialView.clickSaveButton(); @@ -91,7 +91,7 @@ test('can create a partial view in a folder in a folder', async ({umbracoApi, um await umbracoUi.partialView.reloadPartialViewTree(); await umbracoUi.partialView.clickCaretButtonForName(folderName); await umbracoUi.partialView.clickActionsMenuForPartialView(childFolderName); - await umbracoUi.partialView.clickCreateButton(); + await umbracoUi.partialView.clickCreateOptionsActionMenuOption(); await umbracoUi.partialView.clickNewEmptyPartialViewButton(); await umbracoUi.partialView.enterPartialViewName(partialViewName); await umbracoUi.partialView.clickSaveButton(); @@ -161,7 +161,7 @@ test('cannot delete non-empty folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.partialView.clickRootFolderCaretButton(); await umbracoUi.partialView.clickActionsMenuForPartialView(folderName); - await umbracoUi.partialView.deleteFolder(); + await umbracoUi.partialView.clickDeleteAndConfirmButton(); // Assert await umbracoUi.partialView.doesErrorNotificationHaveText(NotificationConstantHelper.error.notEmpty); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/Script.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/Script.spec.ts index fbf775139b..69611cd633 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/Script.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/Script.spec.ts @@ -19,7 +19,7 @@ test('can create a empty script', {tag: '@smoke'}, async ({umbracoApi, umbracoUi // Act await umbracoUi.script.clickActionsMenuAtRoot(); - await umbracoUi.script.clickCreateButton(); + await umbracoUi.script.clickCreateOptionsActionMenuOption(); await umbracoUi.script.clickNewJavascriptFileButton(); await umbracoUi.script.enterScriptName(scriptName); await umbracoUi.script.clickSaveButton(); @@ -38,7 +38,7 @@ test('can create a script with content', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.script.clickActionsMenuAtRoot(); - await umbracoUi.script.clickCreateButton(); + await umbracoUi.script.clickCreateOptionsActionMenuOption(); await umbracoUi.script.clickNewJavascriptFileButton(); await umbracoUi.script.enterScriptName(scriptName); await umbracoUi.script.enterScriptContent(scriptContent); @@ -111,7 +111,7 @@ test('cannot create a script with an empty name', async ({umbracoApi, umbracoUi} // Act await umbracoUi.script.clickActionsMenuAtRoot(); - await umbracoUi.script.clickCreateButton(); + await umbracoUi.script.clickCreateOptionsActionMenuOption(); await umbracoUi.script.clickNewJavascriptFileButton(); await umbracoUi.script.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/ScriptFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/ScriptFolder.spec.ts index 585cf679b6..a7bd693303 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/ScriptFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Script/ScriptFolder.spec.ts @@ -36,7 +36,7 @@ test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => // Act await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickActionsMenuForScript(scriptFolderName); - await umbracoUi.script.deleteFolder(); + await umbracoUi.script.clickDeleteAndConfirmButton(); // Assert //await umbracoUi.script.doesSuccessNotificationHaveText(NotificationConstantHelper.success.deleted); @@ -54,7 +54,7 @@ test('can create a script in a folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickActionsMenuForScript(scriptFolderName); - await umbracoUi.script.clickCreateButton(); + await umbracoUi.script.clickCreateOptionsActionMenuOption(); await umbracoUi.script.clickNewJavascriptFileButton(); await umbracoUi.script.enterScriptName(scriptName); await umbracoUi.script.enterScriptContent(scriptContent); @@ -128,7 +128,7 @@ test('can create a script in a folder in a folder', async ({umbracoApi, umbracoU await umbracoUi.script.reloadScriptTree(); await umbracoUi.script.clickCaretButtonForName(scriptFolderName); await umbracoUi.script.clickActionsMenuForScript(childFolderName); - await umbracoUi.script.clickCreateButton(); + await umbracoUi.script.clickCreateOptionsActionMenuOption(); await umbracoUi.script.clickNewJavascriptFileButton(); await umbracoUi.script.enterScriptName(scriptName); await umbracoUi.script.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/Stylesheet.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/Stylesheet.spec.ts index 11379a031d..0e3e8caf8d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/Stylesheet.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/Stylesheet.spec.ts @@ -21,7 +21,7 @@ test('can create a empty stylesheet', {tag: '@smoke'}, async ({umbracoApi, umbra // Act await umbracoUi.stylesheet.clickActionsMenuAtRoot(); - await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickCreateActionMenuOption(); await umbracoUi.stylesheet.clickNewStylesheetButton(); await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); await umbracoUi.stylesheet.clickSaveButton(); @@ -40,7 +40,7 @@ test('can create a stylesheet with content', async ({umbracoApi, umbracoUi}) => //Act await umbracoUi.stylesheet.clickActionsMenuAtRoot(); - await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickCreateActionMenuOption(); await umbracoUi.stylesheet.clickNewStylesheetButton(); await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); await umbracoUi.stylesheet.enterStylesheetContent(stylesheetContent); @@ -114,7 +114,7 @@ test('cannot create a stylesheet with an empty name', async ({umbracoApi, umbrac // Act await umbracoUi.stylesheet.clickActionsMenuAtRoot(); - await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickCreateActionMenuOption(); await umbracoUi.stylesheet.clickNewStylesheetButton(); await umbracoUi.stylesheet.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/StylesheetFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/StylesheetFolder.spec.ts index 50f414269b..3d70ad63fa 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/StylesheetFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Stylesheet/StylesheetFolder.spec.ts @@ -36,7 +36,7 @@ test('can delete a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => // Act await umbracoUi.stylesheet.reloadStylesheetTree(); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetFolderName); - await umbracoUi.stylesheet.deleteFolder(); + await umbracoUi.stylesheet.clickDeleteAndConfirmButton(); // Assert //await umbracoUi.stylesheet.doesSuccessNotificationHaveText(NotificationConstantHelper.success.deleted); @@ -99,7 +99,7 @@ test('can create a stylesheet in a folder', async ({umbracoApi, umbracoUi}) => { //Act await umbracoUi.stylesheet.reloadStylesheetTree(); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetFolderName); - await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickCreateActionMenuOption(); await umbracoUi.stylesheet.clickNewStylesheetButton(); await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); await umbracoUi.stylesheet.enterStylesheetContent(stylesheetContent); @@ -129,7 +129,7 @@ test('can create a stylesheet in a folder in a folder', async ({umbracoApi, umbr await umbracoUi.stylesheet.reloadStylesheetTree(); await umbracoUi.stylesheet.clickCaretButtonForName(stylesheetFolderName); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(childFolderName); - await umbracoUi.stylesheet.clickCreateButton(); + await umbracoUi.stylesheet.clickCreateActionMenuOption(); await umbracoUi.stylesheet.clickNewStylesheetButton(); await umbracoUi.stylesheet.enterStylesheetName(stylesheetName); await umbracoUi.stylesheet.enterStylesheetContent(stylesheetContent); @@ -157,7 +157,7 @@ test('cannot delete non-empty folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.stylesheet.clickRootFolderCaretButton(); await umbracoUi.stylesheet.clickActionsMenuForStylesheet(stylesheetFolderName); - await umbracoUi.stylesheet.deleteFolder(); + await umbracoUi.stylesheet.clickDeleteAndConfirmButton(); //Assert await umbracoUi.stylesheet.doesErrorNotificationHaveText(NotificationConstantHelper.error.notEmpty); 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 171d879257..6d6e36da83 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 @@ -19,7 +19,7 @@ test('can create a template', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = // Act await umbracoUi.template.clickActionsMenuAtRoot(); - await umbracoUi.template.clickCreateButton(); + await umbracoUi.template.clickCreateActionMenuOption(); await umbracoUi.template.enterTemplateName(templateName); await umbracoUi.template.clickSaveButton(); @@ -86,7 +86,7 @@ test('can delete a template', async ({umbracoApi, umbracoUi}) => { await umbracoUi.template.isErrorNotificationVisible(false); await umbracoUi.template.reloadTemplateTree(); expect(await umbracoApi.template.doesNameExist(templateName)).toBeFalsy(); - await umbracoUi.template.isTemplateRootTreeItemVisible(templateName, false); + await umbracoUi.template.isTemplateRootTreeItemVisible(templateName, false, false); }); test('can set a template as master template', async ({umbracoApi, umbracoUi}) => { @@ -99,7 +99,7 @@ test('can set a template as master template', async ({umbracoApi, umbracoUi}) => // Act await umbracoUi.template.goToTemplate(childTemplateName); await umbracoUi.template.clickChangeMasterTemplateButton(); - await umbracoUi.template.clickButtonWithName(templateName); + await umbracoUi.template.clickModalMenuItemWithName(templateName); await umbracoUi.template.clickChooseButton(); await umbracoUi.template.clickSaveButton(); @@ -378,7 +378,7 @@ test('cannot create a template with an empty name', {tag: '@smoke'}, async ({umb // Act await umbracoUi.template.clickActionsMenuAtRoot(); - await umbracoUi.template.clickCreateButton(); + await umbracoUi.template.clickCreateActionMenuOption(); await umbracoUi.template.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/ContentWithTinyMCERichTextEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/ContentWithTinyMCERichTextEditor.spec.ts index 99eea41a1a..963bf3b551 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/ContentWithTinyMCERichTextEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/Permissions/UserGroup/ContentWithTinyMCERichTextEditor.spec.ts @@ -38,7 +38,7 @@ test('can create content with a rich text editor that has a stylesheet', async ( // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(documentName); await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource, 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 a103a6f7e4..ea32f45f76 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 @@ -91,7 +91,7 @@ test('can create document blueprint with permission enabled', async ({umbracoApi // Act await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); - await umbracoUi.content.clickCreateDocumentBlueprintButton(); + await umbracoUi.content.clickCreateBlueprintActionMenuOption(); await umbracoUi.content.enterDocumentBlueprintName(documentBlueprintName); await umbracoUi.content.clickSaveDocumentBlueprintButton(); @@ -123,7 +123,7 @@ test('can delete content with delete permission enabled', async ({umbracoApi, um // Act await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); - await umbracoUi.content.clickTrashButton(); + await umbracoUi.content.clickTrashActionMenuOption(); await umbracoUi.content.clickConfirmTrashButton(); // Assert @@ -187,7 +187,7 @@ test('can create content with create permission enabled', async ({umbracoApi, um // Act await umbracoUi.content.clickActionsMenuAtRoot(); - await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(rootDocumentTypeName); await umbracoUi.content.enterContentName(testDocumentName); await umbracoUi.content.clickSaveButton(); @@ -247,7 +247,7 @@ test('can publish content with publish permission enabled', async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); - await umbracoUi.content.clickPublishButton(); + await umbracoUi.content.clickPublishActionMenuOption(); await umbracoUi.content.clickConfirmToPublishButton(); // Assert @@ -312,7 +312,7 @@ test('can unpublish content with unpublish permission enabled', async ({umbracoA // Act await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); - await umbracoUi.content.clickUnpublishButton(); + await umbracoUi.content.clickUnpublishActionMenuOption(); await umbracoUi.content.clickConfirmToUnpublishButton(); // Assert @@ -383,7 +383,7 @@ test('can duplicate content with duplicate permission enabled', async ({umbracoA // Act await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); // Duplicate to root - await umbracoUi.content.clickDuplicateToButton(); + await umbracoUi.content.clickDuplicateToActionMenuOption(); await umbracoUi.content.clickLabelWithName('Content'); await umbracoUi.content.clickDuplicateButton(); @@ -425,7 +425,7 @@ test('can move content with move to permission enabled', async ({umbracoApi, umb // Act await umbracoUi.content.clickCaretButtonForContentName(rootDocumentName); await umbracoUi.content.clickActionsMenuForContent(childDocumentOneName); - await umbracoUi.content.clickMoveToButton(); + await umbracoUi.content.clickMoveToActionMenuOption(); await umbracoUi.content.moveToContentWithName([], moveToDocumentName); // Assert @@ -466,7 +466,7 @@ test('can sort children with sort children permission enabled', async ({umbracoA // Act await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); - await umbracoUi.content.clickSortChildrenButton(); + await umbracoUi.content.clickSortChildrenActionMenuOption(); // TODO: uncomment when it is not flaky // const childDocumentOneLocator = await umbracoUi.content.getButtonWithName(childDocumentOneName); @@ -507,7 +507,7 @@ test('can set culture and hostnames with culture and hostnames permission enable // Act await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); - await umbracoUi.content.clickCultureAndHostnamesButton(); + await umbracoUi.content.clickCultureAndHostnamesActionMenuOption(); await umbracoUi.content.clickAddNewDomainButton(); await umbracoUi.content.enterDomain('/domain'); await umbracoUi.content.clickSaveModalButton(); @@ -544,7 +544,7 @@ test.skip('can set public access with public access permission enabled', async ( // Act await umbracoUi.content.clickActionsMenuForContent(rootDocumentName); - await umbracoUi.content.clickPublicAccessButton(); + await umbracoUi.content.clickPublicAccessActionMenuOption(); await umbracoUi.content.addGroupBasedPublicAccess(testMemberGroup, rootDocumentName); // Assert 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 dc174629ab..bb55acd9d8 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -22,8 +22,7 @@ test('can create a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { await umbracoUi.user.goToUsers(); // Act - await umbracoUi.user.clickCreateButton(); - await umbracoUi.user.clickUserButton(); + await umbracoUi.user.clickCreateActionWithOptionName('User'); await umbracoUi.user.enterNameOfTheUser(nameOfTheUser); await umbracoUi.user.enterUserEmail(userEmail); await umbracoUi.user.clickChooseButton(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index 5b63a7271d..94a6c1884a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; @@ -129,7 +130,7 @@ internal sealed class DocumentRepositoryTest : UmbracoIntegrationTest var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var dataValueReferences = - new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); var repository = new DocumentRepository( scopeAccessor, appCaches, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs index 213d6f804a..3b21605d07 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MediaRepositoryTest.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -64,7 +65,7 @@ internal sealed class MediaRepositoryTest : UmbracoIntegrationTest new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var mediaUrlGenerators = new MediaUrlGeneratorCollection(() => Enumerable.Empty()); var dataValueReferences = - new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); var repository = new MediaRepository( scopeAccessor, appCaches, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs index 9f1a190284..d0310af4a1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MemberRepositoryTest.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NPoco; @@ -53,7 +54,7 @@ internal sealed class MemberRepositoryTest : UmbracoIntegrationTest var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var dataValueReferences = - new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); return new MemberRepository( accessor, AppCaches.Disabled, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index b3a8dfa119..973ea658dc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; @@ -272,7 +273,7 @@ internal sealed class TemplateRepositoryTest : UmbracoIntegrationTest var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); var dataValueReferences = - new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); var contentRepo = new DocumentRepository( scopeAccessor, AppCaches.Disabled, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceVariantTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceVariantTests.cs new file mode 100644 index 0000000000..b93800fe91 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceVariantTests.cs @@ -0,0 +1,151 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +// ReSharper disable CommentTypo +// ReSharper disable StringLiteralTypo +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +internal sealed class ContentServiceVariantTests : UmbracoIntegrationTest +{ + private IContentService ContentService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + /// + /// Provides both happy path with correctly cased cultures, and originally failing test cases for + /// https://github.com/umbraco/Umbraco-CMS/issues/19287, where the culture codes are provided with inconsistent casing. + /// + [TestCase("en-US", "en-US", "en-US")] + [TestCase("en-us", "en-us", "en-us")] + [TestCase("en-US", "en-US", "en-us")] + [TestCase("en-us", "en-US", "en-US")] + [TestCase("en-US", "en-us", "en-US")] + public async Task Can_Save_And_Publish_With_Inconsistent_Provision_Of_Culture_Codes(string cultureNameCultureCode, string valueCultureCode, string publishCultureCode) + { + var contentType = await SetupVariantTest(); + + IContent content = ContentService.Create("Test Item", Constants.System.Root, contentType); + content.SetCultureName("Test item", cultureNameCultureCode); + content.SetValue("title", "Title", valueCultureCode); + ContentService.Save(content); + + var publishResult = ContentService.Publish(content, [publishCultureCode]); + Assert.IsTrue(publishResult.Success); + + content = ContentService.GetById(content.Key)!; + Assert.Multiple(() => + { + Assert.IsTrue(content.Published); + Assert.AreEqual(1, content.PublishedCultures.Count()); + Assert.AreEqual("en-US", content.PublishedCultures.FirstOrDefault()); + }); + } + + [TestCase("en-US", "en-US", "en-US")] + [TestCase("en-us", "en-us", "en-us")] + [TestCase("en-US", "en-US", "en-us")] + [TestCase("en-us", "en-US", "en-US")] + [TestCase("en-US", "en-us", "en-US")] + public async Task Can_Unpublish_With_Inconsistent_Provision_Of_Culture_Codes(string cultureNameCultureCode, string valueCultureCode, string unpublishCultureCode) + { + var contentType = await SetupVariantTest(); + + IContent content = ContentService.Create("Test Item", Constants.System.Root, contentType); + content.SetCultureName("Test item", cultureNameCultureCode); + content.SetValue("title", "Title", valueCultureCode); + ContentService.Save(content); + // use correctly cased culture code to publish + ContentService.Publish(content, ["en-US"]); + + var unpublishResult = ContentService.Unpublish(content, unpublishCultureCode); + Assert.IsTrue(unpublishResult.Success); + + content = ContentService.GetById(content.Key)!; + Assert.Multiple(() => + { + Assert.IsFalse(content.Published); + Assert.AreEqual(0, content.PublishedCultures.Count()); + }); + } + + [TestCase("en-US", "en-US", "en-US")] + [TestCase("en-us", "en-us", "en-us")] + [TestCase("en-US", "en-US", "en-us")] + [TestCase("en-us", "en-US", "en-US")] + [TestCase("en-US", "en-us", "en-US")] + public async Task Can_Publish_Branch_With_Inconsistent_Provision_Of_Culture_Codes(string cultureNameCultureCode, string valueCultureCode, string publishCultureCode) + { + var contentType = await SetupVariantTest(); + + IContent root = ContentService.Create("Root", Constants.System.Root, contentType); + root.SetCultureName("Root", cultureNameCultureCode); + root.SetValue("title", "Root Title", valueCultureCode); + ContentService.Save(root); + + var child = ContentService.Create("Child", root.Id, contentType); + child.SetCultureName("Child", cultureNameCultureCode); + child.SetValue("title", "Child Title", valueCultureCode); + ContentService.Save(child); + + var publishResult = ContentService.PublishBranch(root, PublishBranchFilter.All, [publishCultureCode]); + Assert.AreEqual(2, publishResult.Count()); + Assert.IsTrue(publishResult.First().Success); + Assert.IsTrue(publishResult.Last().Success); + + root = ContentService.GetById(root.Key)!; + Assert.Multiple(() => + { + Assert.IsTrue(root.Published); + Assert.AreEqual(1, root.PublishedCultures.Count()); + Assert.AreEqual("en-US", root.PublishedCultures.FirstOrDefault()); + }); + + child = ContentService.GetById(child.Key)!; + Assert.Multiple(() => + { + Assert.IsTrue(child.Published); + Assert.AreEqual(1, child.PublishedCultures.Count()); + Assert.AreEqual("en-US", child.PublishedCultures.FirstOrDefault()); + }); + } + + private async Task SetupVariantTest() + { + var key = Guid.NewGuid(); + var contentType = new ContentTypeBuilder() + .WithAlias("variantContent") + .WithName("Variant Content") + .WithKey(key) + .WithContentVariation(ContentVariation.Culture) + .AddPropertyGroup() + .WithAlias("content") + .WithName("Content") + .WithSupportsPublishing(true) + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithVariations(ContentVariation.Culture) + .Done() + .Done() + .Build(); + + contentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + + contentType.AllowedContentTypes = [new ContentTypeSort(contentType.Key, 0, contentType.Alias)]; + await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey); + + return contentType; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/IntExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/IntExtensionsTests.cs new file mode 100644 index 0000000000..46cab0df54 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/IntExtensionsTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using NUnit.Framework; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions; + +[TestFixture] +public class IntExtensionsTests +{ + [TestCase(20, "00000014-0000-0000-0000-000000000000")] + [TestCase(106, "0000006a-0000-0000-0000-000000000000")] + [TestCase(999999, "000f423f-0000-0000-0000-000000000000")] + [TestCase(555555555, "211d1ae3-0000-0000-0000-000000000000")] + public void ToGuid_Creates_Expected_Guid(int input, string expected) + { + var result = input.ToGuid(); + Assert.AreEqual(expected, result.ToString()); + } + + [TestCase("00000014-0000-0000-0000-000000000000", 20)] + [TestCase("0000006a-0000-0000-0000-000000000000", 106)] + [TestCase("000f423f-0000-0000-0000-000000000000", 999999)] + [TestCase("211d1ae3-0000-0000-0000-000000000000", 555555555)] + [TestCase("0d93047e-558d-4311-8a9d-b89e6fca0337", null)] + public void TryParseFromGuid_Parses_Expected_Integer(string input, int? expected) + { + var result = IntExtensions.TryParseFromGuid(Guid.Parse(input), out int? intValue); + if (expected is null) + { + Assert.IsFalse(result); + Assert.IsFalse(intValue.HasValue); + } + else + { + Assert.IsTrue(result); + Assert.AreEqual(expected, intValue.Value); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs index 879400f79a..fc56535200 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListEditorPropertyValueEditorTests.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; @@ -148,7 +149,7 @@ public class BlockListEditorPropertyValueEditorTests new DataEditorAttribute("alias"), new BlockListEditorDataConverter(jsonSerializer), new(new DataEditorCollection(() => [])), - new DataValueReferenceFactoryCollection(Enumerable.Empty), + new DataValueReferenceFactoryCollection(Enumerable.Empty, Mock.Of>()), Mock.Of(), Mock.Of(), localizedTextServiceMock.Object, diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 16ea5c11f9..f83b779c63 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; @@ -35,7 +36,7 @@ public class DataValueEditorReuseTests Mock.Of())); _propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); - _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty); + _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty, new NullLogger()); var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of(), Mock.Of()); _dataValueEditorFactoryMock diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs index 33f4f307f8..43757f064c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Microsoft.Extensions.Logging.Abstractions; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -49,7 +50,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAllReferences_All_Variants_With_IDataValueReferenceFactory() { - var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield()); + var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield(), new NullLogger()); // label does not implement IDataValueReference var labelEditor = new LabelPropertyEditor( @@ -93,7 +94,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAllReferences_All_Variants_With_IDataValueReference_Editor() { - var collection = new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + var collection = new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); // mediaPicker does implement IDataValueReference var mediaPicker = new MediaPicker3PropertyEditor( @@ -137,7 +138,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAllReferences_Invariant_With_IDataValueReference_Editor() { - var collection = new DataValueReferenceFactoryCollection(() => Enumerable.Empty()); + var collection = new DataValueReferenceFactoryCollection(() => Enumerable.Empty(), new NullLogger()); // mediaPicker does implement IDataValueReference var mediaPicker = new MediaPicker3PropertyEditor( @@ -181,7 +182,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAutomaticRelationTypesAliases_ContainsDefault() { - var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty); + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty, new NullLogger()); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray(); @@ -193,7 +194,7 @@ public class DataValueReferenceFactoryCollectionTests [Test] public void GetAutomaticRelationTypesAliases_ContainsCustom() { - var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield()); + var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield(), new NullLogger()); var labelPropertyEditor = new LabelPropertyEditor(DataValueEditorFactory, IOHelper); var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => labelPropertyEditor.Yield())); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/StringExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/StringExtensionsTests.cs index 58b73ccf83..d0f93044c3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/StringExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/StringExtensionsTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -395,4 +396,17 @@ public class StringExtensionsTests var ids = input.GetIdsFromPathReversed(); Assert.AreEqual(expected, string.Join(",", ids)); } + + [TestCase(null, null)] + [TestCase("", "")] + [TestCase("*", "*")] + [TestCase("en", "en")] + [TestCase("EN", "en")] + [TestCase("en-US", "en-US")] + [TestCase("en-gb", "en-GB")] + public void EnsureCultureCode_ReturnsExpectedResult(string? culture, string? expected) => Assert.AreEqual(expected, culture.EnsureCultureCode()); + + [Test] + [Platform(Include = "Win")] + public void EnsureCultureCode_ThrowsOnUnrecognisedCode() => Assert.Throws(() => "xxx-xxx".EnsureCultureCode()); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs index 399633da2a..3a7caf1f6a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs @@ -39,28 +39,42 @@ public class HtmlImageSourceParserTests Assert.AreEqual(UdiParser.Parse("umb://media-type/B726D735E4C446D58F703F3FBCFC97A5"), result[1]); } - [Test] - public void Remove_Image_Sources() - { - var imageSourceParser = new HtmlImageSourceParser(Mock.Of()); - - var result = imageSourceParser.RemoveImageSources(@"

+ [TestCase( + @"

-

"); - - Assert.AreEqual( - @"

+

", + ExpectedResult = @"

", - result); + TestName = "Remove image source with data-udi set")] + [TestCase( + @"", + ExpectedResult = @"", + TestName = "Remove image source but keep querystring")] + [TestCase( + @"", + ExpectedResult = @"", + TestName = "Remove image source with empty src")] + [TestCase( + @"", + ExpectedResult = @"", + TestName = "Do not remove image source without data-udi set")] + [Category("Remove image sources")] + public string Remove_Image_Sources(string sourceHtml) + { + var imageSourceParser = new HtmlImageSourceParser(Mock.Of()); + + var actual = imageSourceParser.RemoveImageSources(sourceHtml); + + return actual; } [Test] @@ -146,6 +160,10 @@ public class HtmlImageSourceParserTests @"
", ExpectedResult = @"
", TestName = "Filled source is overwritten with data-udi set")] + [TestCase( + @"
", + ExpectedResult = @"
", + TestName = "Order of attributes does not matter")] [TestCase( @"
", ExpectedResult = @"
", @@ -158,6 +176,10 @@ public class HtmlImageSourceParserTests @"
", ExpectedResult = @"
", TestName = "Parameters are prefixed")] + [TestCase( + @"
", + ExpectedResult = @"
", + TestName = "Parameters are prefixed (order of attributes reversed)")] [TestCase( @"