diff --git a/Directory.Build.props b/Directory.Build.props index a3b7db5b37..7e8128fd83 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -14,7 +14,7 @@ en-US enable nullable - true + false enable true false diff --git a/Directory.Packages.props b/Directory.Packages.props index d207cb2f8b..bc0a2db4fd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ - + diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index 1558329746..0139bb61ce 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -46,7 +46,9 @@ public static class UmbracoBuilderAuthExtensions Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash)) .SetRevocationEndpointUris( Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash), - Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash)); + Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash)) + .SetUserInfoEndpointUris( + Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash)); // Enable authorization code flow with PKCE options @@ -62,7 +64,8 @@ public static class UmbracoBuilderAuthExtensions .UseAspNetCore() .EnableAuthorizationEndpointPassthrough() .EnableTokenEndpointPassthrough() - .EnableEndSessionEndpointPassthrough(); + .EnableEndSessionEndpointPassthrough() + .EnableUserInfoEndpointPassthrough(); // Enable reference tokens // - see https://documentation.openiddict.com/configuration/token-storage.html diff --git a/src/Umbraco.Cms.Api.Common/Security/Paths.cs b/src/Umbraco.Cms.Api.Common/Security/Paths.cs index 61e89ed3a3..916ab228ff 100644 --- a/src/Umbraco.Cms.Api.Common/Security/Paths.cs +++ b/src/Umbraco.Cms.Api.Common/Security/Paths.cs @@ -31,6 +31,8 @@ public static class Paths public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke"); + public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo"); + // NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}"; } diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs index 4637055c11..1db4d9d395 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs @@ -15,6 +15,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content; [ApiExplorerSettings(GroupName = "Content")] [LocalizeFromAcceptLanguageHeader] [ValidateStartItem] +[AddVaryHeader] [OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)] public abstract class ContentApiControllerBase : DeliveryApiControllerBase { diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/CurrentMemberController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/CurrentMemberController.cs new file mode 100644 index 0000000000..41d1970d06 --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/CurrentMemberController.cs @@ -0,0 +1,28 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Server.AspNetCore; +using Umbraco.Cms.Api.Delivery.Routing; +using Umbraco.Cms.Core.DeliveryApi; + +namespace Umbraco.Cms.Api.Delivery.Controllers.Security; + +[ApiVersion("1.0")] +[ApiController] +[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)] +[ApiExplorerSettings(IgnoreApi = true)] +[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] +public class CurrentMemberController : DeliveryApiControllerBase +{ + private readonly ICurrentMemberClaimsProvider _currentMemberClaimsProvider; + + public CurrentMemberController(ICurrentMemberClaimsProvider currentMemberClaimsProvider) + => _currentMemberClaimsProvider = currentMemberClaimsProvider; + + [HttpGet("userinfo")] + public async Task Userinfo() + { + Dictionary claims = await _currentMemberClaimsProvider.GetClaimsAsync(); + return Ok(claims); + } +} diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs index 77f2cae93c..89065a5d2e 100644 --- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs @@ -61,6 +61,7 @@ public static class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddScoped(); builder.Services.ConfigureOptions(); diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/AddVaryHeaderAttribute.cs b/src/Umbraco.Cms.Api.Delivery/Filters/AddVaryHeaderAttribute.cs new file mode 100644 index 0000000000..f8905e72d5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Filters/AddVaryHeaderAttribute.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Umbraco.Cms.Api.Delivery.Filters; + +public sealed class AddVaryHeaderAttribute : ActionFilterAttribute +{ + private const string Vary = "Accept-Language, Preview, Start-Item"; + + public override void OnResultExecuting(ResultExecutingContext context) + => context.HttpContext.Response.Headers.Vary = context.HttpContext.Response.Headers.Vary.Count > 0 + ? $"{context.HttpContext.Response.Headers.Vary}, {Vary}" + : Vary; +} diff --git a/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiVersionAwareJsonConverterBase.cs b/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiVersionAwareJsonConverterBase.cs new file mode 100644 index 0000000000..93b5b9f49b --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiVersionAwareJsonConverterBase.cs @@ -0,0 +1,88 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core.DeliveryApi; + +namespace Umbraco.Cms.Api.Delivery.Json; + +public abstract class DeliveryApiVersionAwareJsonConverterBase : JsonConverter +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly JsonConverter _defaultConverter = (JsonConverter)JsonSerializerOptions.Default.GetConverter(typeof(T)); + + public DeliveryApiVersionAwareJsonConverterBase(IHttpContextAccessor httpContextAccessor) + => _httpContextAccessor = httpContextAccessor; + + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => _defaultConverter.Read(ref reader, typeToConvert, options); + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + Type type = typeof(T); + var apiVersion = GetApiVersion(); + + // Get the properties in the specified order + PropertyInfo[] properties = type.GetProperties().OrderBy(GetPropertyOrder).ToArray(); + + writer.WriteStartObject(); + + foreach (PropertyInfo property in properties) + { + // Filter out properties based on the API version + var include = apiVersion is null || ShouldIncludeProperty(property, apiVersion.Value); + + if (include is false) + { + continue; + } + + var propertyName = property.Name; + writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName); + JsonSerializer.Serialize(writer, property.GetValue(value), options); + } + + writer.WriteEndObject(); + } + + private int? GetApiVersion() + { + HttpContext? httpContext = _httpContextAccessor.HttpContext; + ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion(); + + return apiVersion?.MajorVersion; + } + + private int GetPropertyOrder(PropertyInfo prop) + { + var attribute = prop.GetCustomAttribute(); + return attribute?.Order ?? 0; + } + + /// + /// Determines whether a property should be included based on version bounds. + /// + /// The property info. + /// An integer representing an API version. + /// true if the property should be included; otherwise, false. + private bool ShouldIncludeProperty(PropertyInfo propertyInfo, int version) + { + var attribute = propertyInfo + .GetCustomAttributes(typeof(IncludeInApiVersionAttribute), false) + .FirstOrDefault(); + + if (attribute is not IncludeInApiVersionAttribute apiVersionAttribute) + { + return true; // No attribute means include the property + } + + // Check if the version is within the specified bounds + var isWithinMinVersion = apiVersionAttribute.MinVersion.HasValue is false || version >= apiVersionAttribute.MinVersion.Value; + var isWithinMaxVersion = apiVersionAttribute.MaxVersion.HasValue is false || version <= apiVersionAttribute.MaxVersion.Value; + + return isWithinMinVersion && isWithinMaxVersion; + } +} diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/ContentTypeFilter.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/ContentTypeFilter.cs index 13d9dc77ec..50c390c766 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/ContentTypeFilter.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/ContentTypeFilter.cs @@ -1,6 +1,5 @@ using Umbraco.Cms.Api.Delivery.Indexing.Filters; using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Querying.Filters; @@ -15,15 +14,15 @@ public sealed class ContentTypeFilter : IFilterHandler /// public FilterOption BuildFilterOption(string filter) { - var alias = filter.Substring(ContentTypeSpecifier.Length); + var filterValue = filter.Substring(ContentTypeSpecifier.Length); + var negate = filterValue.StartsWith('!'); + var aliases = filterValue.TrimStart('!').Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); return new FilterOption { FieldName = ContentTypeFilterIndexer.FieldName, - Values = alias.IsNullOrWhiteSpace() == false - ? new[] { alias.TrimStart('!') } - : Array.Empty(), - Operator = alias.StartsWith('!') + Values = aliases, + Operator = negate ? FilterOperation.IsNot : FilterOperation.Is }; diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs b/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs index 1576d0037f..2b47ea166e 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs @@ -1,5 +1,6 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; @@ -7,16 +8,44 @@ namespace Umbraco.Cms.Api.Delivery.Querying; public abstract class QueryOptionBase { - private readonly IPublishedContentCache _publishedContentCache; private readonly IRequestRoutingService _requestRoutingService; + private readonly IRequestPreviewService _requestPreviewService; + private readonly IRequestCultureService _requestCultureService; + private readonly IApiDocumentUrlService _apiDocumentUrlService; - + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public QueryOptionBase( IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService) + : this( + requestRoutingService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] + public QueryOptionBase( + IPublishedContentCache publishedContentCache, + IRequestRoutingService requestRoutingService, + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IApiDocumentUrlService apiDocumentUrlService) + : this(requestRoutingService, requestPreviewService, requestCultureService, apiDocumentUrlService) + { + } + + public QueryOptionBase( + IRequestRoutingService requestRoutingService, + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IApiDocumentUrlService apiDocumentUrlService) { - _publishedContentCache = publishedContentCache; _requestRoutingService = requestRoutingService; + _requestPreviewService = requestPreviewService; + _requestCultureService = requestCultureService; + _apiDocumentUrlService = apiDocumentUrlService; } protected Guid? GetGuidFromQuery(string queryStringValue) @@ -33,8 +62,9 @@ public abstract class QueryOptionBase // Check if the passed value is a path of a content item var contentRoute = _requestRoutingService.GetContentRoute(queryStringValue); - IPublishedContent? contentItem = _publishedContentCache.GetByRoute(contentRoute); - - return contentItem?.Key; + return _apiDocumentUrlService.GetDocumentKeyByRoute( + contentRoute, + _requestCultureService.GetRequestedCulture(), + _requestPreviewService.IsPreview()); } } diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs index 3f3a18f00c..7a3a793118 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/AncestorsSelector.cs @@ -2,44 +2,77 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Delivery.Indexing.Selectors; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services.Navigation; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Querying.Selectors; public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler { - private readonly IPublishedContentCache _publishedContentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; - private readonly IRequestPreviewService _requestPreviewService; private const string AncestorsSpecifier = "ancestors:"; + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public AncestorsSelector( IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService, IDocumentNavigationQueryService navigationQueryService, IRequestPreviewService requestPreviewService) - : base(publishedContentCache, requestRoutingService) + : this( + requestRoutingService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + navigationQueryService) { - _publishedContentCache = publishedContentCache; - _navigationQueryService = navigationQueryService; - _requestPreviewService = requestPreviewService; } - [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public AncestorsSelector( IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService, IDocumentNavigationQueryService navigationQueryService) - : this(publishedContentCache, requestRoutingService, navigationQueryService, StaticServiceProvider.Instance.GetRequiredService()) + : this( + requestRoutingService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + navigationQueryService) { } [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] public AncestorsSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService) - : this(publishedContentCache, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService()) + : this( + requestRoutingService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public AncestorsSelector( + IRequestRoutingService requestRoutingService, + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IApiDocumentUrlService apiDocumentUrlService, + IDocumentNavigationQueryService navigationQueryService) + : base(requestRoutingService, requestPreviewService, requestCultureService, apiDocumentUrlService) + => _navigationQueryService = navigationQueryService; + + [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + public AncestorsSelector( + IRequestRoutingService requestRoutingService, + IPublishedContentCache publishedContentCache, + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IApiDocumentUrlService apiDocumentUrlService, + IDocumentNavigationQueryService navigationQueryService) + : this(requestRoutingService, requestPreviewService, requestCultureService, apiDocumentUrlService, navigationQueryService) { } @@ -53,7 +86,7 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler var fieldValue = selector[AncestorsSpecifier.Length..]; Guid? id = GetGuidFromQuery(fieldValue); - if (id is null) + if (id is null || _navigationQueryService.TryGetAncestorsKeys(id.Value, out IEnumerable ancestorKeys) is false) { // Setting the Value to "" since that would yield no results. // It won't be appropriate to return null here since if we reached this, @@ -65,24 +98,10 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler }; } - IPublishedContent? contentItem = _publishedContentCache.GetById(_requestPreviewService.IsPreview(), id.Value); - - if (contentItem is null) - { - // no such content item, make sure the selector does not yield any results - return new SelectorOption - { - FieldName = AncestorsSelectorIndexer.FieldName, - Values = Array.Empty() - }; - } - - var ancestorKeys = contentItem.Ancestors(_publishedContentCache, _navigationQueryService).Select(a => a.Key.ToString("D")).ToArray(); - return new SelectorOption { FieldName = AncestorsSelectorIndexer.FieldName, - Values = ancestorKeys + Values = ancestorKeys.Select(key => key.ToString("D")).ToArray() }; } } diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/ChildrenSelector.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/ChildrenSelector.cs index 9392ce8e02..ba986e0812 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/ChildrenSelector.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/ChildrenSelector.cs @@ -1,5 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Delivery.Indexing.Selectors; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; @@ -9,8 +11,33 @@ public sealed class ChildrenSelector : QueryOptionBase, ISelectorHandler { private const string ChildrenSpecifier = "children:"; + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public ChildrenSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService) - : base(publishedContentCache, requestRoutingService) + : this( + requestRoutingService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] + public ChildrenSelector( + IPublishedContentCache publishedContentCache, + IRequestRoutingService requestRoutingService, + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IApiDocumentUrlService apiDocumentUrlService) + : this(requestRoutingService, requestPreviewService, requestCultureService, apiDocumentUrlService) + { + } + + public ChildrenSelector( + IRequestRoutingService requestRoutingService, + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IApiDocumentUrlService apiDocumentUrlService) + : base(requestRoutingService, requestPreviewService, requestCultureService, apiDocumentUrlService) { } diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/DescendantsSelector.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/DescendantsSelector.cs index 2a7512746e..7ce0c066c4 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/DescendantsSelector.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Selectors/DescendantsSelector.cs @@ -1,5 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Delivery.Indexing.Selectors; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; @@ -9,8 +11,33 @@ public sealed class DescendantsSelector : QueryOptionBase, ISelectorHandler { private const string DescendantsSpecifier = "descendants:"; + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public DescendantsSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService) - : base(publishedContentCache, requestRoutingService) + : this( + requestRoutingService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] + public DescendantsSelector( + IPublishedContentCache publishedContentCache, + IRequestRoutingService requestRoutingService, + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IApiDocumentUrlService apiDocumentUrlService) + : this(requestRoutingService, requestPreviewService, requestCultureService, apiDocumentUrlService) + { + } + + public DescendantsSelector( + IRequestRoutingService requestRoutingService, + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IApiDocumentUrlService apiDocumentUrlService) + : base(requestRoutingService, requestPreviewService, requestCultureService, apiDocumentUrlService) { } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/CurrentMemberClaimsProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/CurrentMemberClaimsProvider.cs new file mode 100644 index 0000000000..8a358f11a8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Services/CurrentMemberClaimsProvider.cs @@ -0,0 +1,44 @@ +using OpenIddict.Abstractions; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Api.Delivery.Services; + +// NOTE: this is public and unsealed to allow overriding the default claims with minimal effort. +public class CurrentMemberClaimsProvider : ICurrentMemberClaimsProvider +{ + private readonly IMemberManager _memberManager; + + public CurrentMemberClaimsProvider(IMemberManager memberManager) + => _memberManager = memberManager; + + public virtual async Task> GetClaimsAsync() + { + MemberIdentityUser? memberIdentityUser = await _memberManager.GetCurrentMemberAsync(); + return memberIdentityUser is not null + ? await GetClaimsForMemberIdentityAsync(memberIdentityUser) + : throw new InvalidOperationException("Could not retrieve the current member. This method should only ever be invoked when a member has been authorized."); + } + + protected virtual async Task> GetClaimsForMemberIdentityAsync(MemberIdentityUser memberIdentityUser) + { + var claims = new Dictionary + { + [OpenIddictConstants.Claims.Subject] = memberIdentityUser.Key + }; + + if (memberIdentityUser.Name is not null) + { + claims[OpenIddictConstants.Claims.Name] = memberIdentityUser.Name; + } + + if (memberIdentityUser.Email is not null) + { + claims[OpenIddictConstants.Claims.Email] = memberIdentityUser.Email; + } + + claims[OpenIddictConstants.Claims.Role] = await _memberManager.GetRolesAsync(memberIdentityUser); + + return claims; + } +} diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs index e79a2cbdd7..92aff37011 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs @@ -49,9 +49,8 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS _documentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); IEnumerable rootContent = rootKeys - .Select(_publishedContentCache.GetById) - .WhereNotNull() - .Where(x => x.IsPublished() != _requestPreviewService.IsPreview()); + .Select(rootKey => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), rootKey)) + .WhereNotNull(); _requestedStartContent = Guid.TryParse(headerValue, out Guid key) ? rootContent.FirstOrDefault(c => c.Key == key) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs index 669c2cdc93..1c1dad37c9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs @@ -18,21 +18,18 @@ public abstract class CreateDocumentControllerBase : DocumentControllerBase protected async Task HandleRequest(CreateDocumentRequestModel requestModel, Func> authorizedHandler) { - // TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages. - // The values are ignored in the ContentEditingService + // We intentionally don't pass in cultures here. + // This is to support the client sending values for all cultures even if the user doesn't have access to the language. + // Values for unauthorized languages are later ignored in the ContentEditingService. + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id), + AuthorizationPolicies.ContentPermissionByResource); - // IEnumerable cultures = requestModel.Variants - // .Where(v => v.Culture is not null) - // .Select(v => v.Culture!); - // AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( - // User, - // ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures), - // AuthorizationPolicies.ContentPermissionByResource); - // - // if (!authorizationResult.Succeeded) - // { - // return Forbidden(); - // } + if (authorizationResult.Succeeded is false) + { + return Forbidden(); + } return await authorizedHandler(); } 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 f3b7cb1caf..0ab15f0e7f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs @@ -1,4 +1,5 @@ -using Asp.Versioning; +using System.Text.Json.Serialization; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; @@ -26,12 +27,17 @@ public class SearchDocumentItemController : DocumentItemControllerBase public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) => await SearchFromParent(cancellationToken, query, skip, take); + [NonAction] + [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] + public async Task SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null) + => await SearchFromParentWithAllowedTypes(cancellationToken, query, skip, take, parentId); + [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null) + public async Task SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable? allowedDocumentTypes = null) { - PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, skip, take); + PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_documentPresentationFactory.CreateItemResponseModel), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs index 4b585e78b9..a97a73e712 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs @@ -17,21 +17,18 @@ public abstract class UpdateDocumentControllerBase : DocumentControllerBase protected async Task HandleRequest(Guid id, UpdateDocumentRequestModel requestModel, Func> authorizedHandler) { - // TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages. - // The values are ignored in the ContentEditingService + // We intentionally don't pass in cultures here. + // This is to support the client sending values for all cultures even if the user doesn't have access to the language. + // Values for unauthorized languages are later ignored in the ContentEditingService. + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); - // IEnumerable cultures = requestModel.Variants - // .Where(v => v.Culture is not null) - // .Select(v => v.Culture!); - // AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( - // User, - // ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures), - // AuthorizationPolicies.ContentPermissionByResource); - // - // if (!authorizationResult.Succeeded) - // { - // return Forbidden(); - // } + if (authorizationResult.Succeeded is false) + { + return Forbidden(); + } return await authorizedHandler(); } 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 81d4f17748..c7cccd0cea 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs @@ -26,12 +26,18 @@ public class SearchMediaItemController : MediaItemControllerBase public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) => await SearchFromParent(cancellationToken, query, skip, take, null); + [NonAction] + [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] + [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] + public async Task SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null) + => await SearchFromParentWithAllowedTypes(cancellationToken, query, skip, take, parentId); + [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null) + public async Task SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable? allowedMediaTypes = null) { - PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, skip, take); + PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_mediaPresentationFactory.CreateItemResponseModel), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/SearchMemberItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/SearchMemberItemController.cs index 21b5b7fad5..187dc239b3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/SearchMemberItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/SearchMemberItemController.cs @@ -21,12 +21,17 @@ public class SearchMemberItemController : MemberItemControllerBase _memberPresentationFactory = memberPresentationFactory; } + [NonAction] + [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] + public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) + => await SearchWithAllowedTypes(cancellationToken, query, skip, take); + [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) + public async Task SearchWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, [FromQuery]IEnumerable? allowedMemberTypes = null) { - PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Member, query, skip, take); + PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Member, query, null, allowedMemberTypes, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_memberPresentationFactory.CreateItemResponseModel), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Searcher/QuerySearcherController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Searcher/QuerySearcherController.cs index 7f5c545095..f3f7d0acad 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Searcher/QuerySearcherController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Searcher/QuerySearcherController.cs @@ -1,5 +1,6 @@ using Asp.Versioning; using Examine; +using Examine.Lucene.Search; using Examine.Search; using Lucene.Net.QueryParsers.Classic; using Microsoft.AspNetCore.Http; @@ -52,12 +53,13 @@ public class QuerySearcherController : SearcherControllerBase ISearchResults results; // NativeQuery will work for a single word/phrase too (but depends on the implementation) the lucene one will work. + // Due to examine changes we need to supply the skipTakeMaxResults, see https://github.com/umbraco/Umbraco-CMS/issues/17920 for more info try { results = searcher .CreateQuery() .NativeQuery(term) - .Execute(QueryOptions.SkipTake(skip, take)); + .Execute(new LuceneQueryOptions(skip, take, skipTakeMaxResults: skip + take)); } catch (ParseException) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs index 3e71b4cc31..cada1031e3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs @@ -275,9 +275,12 @@ public class BackOfficeController : SecurityControllerBase [MapToApiVersion("1.0")] public async Task Signout(CancellationToken cancellationToken) { - var userName = await GetUserNameFromAuthCookie(); + AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); + var userName = cookieAuthResult.Principal?.Identity?.Name; + var userId = cookieAuthResult.Principal?.Identity?.GetUserId(); await _backOfficeSignInManager.SignOutAsync(); + _backOfficeUserManager.NotifyLogoutSuccess(cookieAuthResult.Principal ?? User, userId); _logger.LogInformation( "User {UserName} from IP address {RemoteIpAddress} has logged out", diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs index 39c71b3c55..11c9a36e25 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; @@ -55,8 +56,8 @@ public class BackOfficeGraphicsController : Controller private IActionResult HandleFileRequest(string virtualPath) { - var filePath = Path.Combine(Constants.SystemDirectories.Umbraco, virtualPath).TrimStart(Constants.CharArrays.Tilde); - var fileInfo = _webHostEnvironment.WebRootFileProvider.GetFileInfo(filePath); + var filePath = $"{Constants.SystemDirectories.Umbraco}/{virtualPath}".TrimStart(Constants.CharArrays.Tilde); + IFileInfo fileInfo = _webHostEnvironment.WebRootFileProvider.GetFileInfo(filePath); if (fileInfo.PhysicalPath is null) { diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/AuditLogBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/AuditLogBuilderExtensions.cs index a962ba558e..edca89e56b 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/AuditLogBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/AuditLogBuilderExtensions.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Security; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Api.Management.DependencyInjection; @@ -9,6 +11,13 @@ internal static class AuditLogBuilderExtensions internal static IUmbracoBuilder AddAuditLogs(this IUmbracoBuilder builder) { builder.Services.AddTransient(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); return builder; } diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs index 6d0e40e2b1..ba6ebfaf9a 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs @@ -1,31 +1,38 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.Navigation; -using Umbraco.Cms.Core.Web; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Factories; public class DocumentUrlFactory : IDocumentUrlFactory { - private readonly IDocumentUrlService _documentUrlService; + private readonly IPublishedUrlInfoProvider _publishedUrlInfoProvider; + [Obsolete("Use the constructor that takes all dependencies, scheduled for removal in v16")] public DocumentUrlFactory(IDocumentUrlService documentUrlService) + : this(StaticServiceProvider.Instance.GetRequiredService()) { - _documentUrlService = documentUrlService; + } + + [Obsolete("Use the constructor that takes all dependencies, scheduled for removal in v16")] + public DocumentUrlFactory(IDocumentUrlService documentUrlService, IPublishedUrlInfoProvider publishedUrlInfoProvider) + : this(publishedUrlInfoProvider) + { + + } + + public DocumentUrlFactory(IPublishedUrlInfoProvider publishedUrlInfoProvider) + { + _publishedUrlInfoProvider = publishedUrlInfoProvider; } public async Task> CreateUrlsAsync(IContent content) { - IEnumerable urlInfos = await _documentUrlService.ListUrlsAsync(content.Key); + ISet urlInfos = await _publishedUrlInfoProvider.GetAllAsync(content); return urlInfos .Where(urlInfo => urlInfo.IsUrl) diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs index f87a0aaec8..a64d069704 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs @@ -6,5 +6,6 @@ namespace Umbraco.Cms.Api.Management.Factories; public interface IDocumentUrlFactory { Task> CreateUrlsAsync(IContent content); + Task> CreateUrlSetsAsync(IEnumerable contentItems); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index 99206da2ac..e1ee38d51a 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -143,6 +143,10 @@ public class UserPresentationFactory : IUserPresentationFactory KeepUserLoggedIn = _securitySettings.KeepUserLoggedIn, UsernameIsEmail = _securitySettings.UsernameIsEmail, PasswordConfiguration = _passwordConfigurationPresentationFactory.CreatePasswordConfigurationResponseModel(), + + // You should not be able to change any password or set 2fa if any providers has deny local login set. + AllowChangePassword = _externalLoginProviders.HasDenyLocalLogin() is false, + AllowTwoFactor = _externalLoginProviders.HasDenyLocalLogin() is false, }; return await Task.FromResult(model); diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 2334865491..af29c9fd8d 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -10086,6 +10086,17 @@ "type": "string", "format": "uuid" } + }, + { + "name": "allowedDocumentTypes", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } } ], "responses": { @@ -15764,6 +15775,17 @@ "type": "string", "format": "uuid" } + }, + { + "name": "allowedMediaTypes", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } } ], "responses": { @@ -19638,6 +19660,17 @@ "format": "int32", "default": 100 } + }, + { + "name": "allowedMemberTypes", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } } ], "responses": { @@ -35929,6 +35962,8 @@ }, "CurrenUserConfigurationResponseModel": { "required": [ + "allowChangePassword", + "allowTwoFactor", "keepUserLoggedIn", "passwordConfiguration", "usernameIsEmail" @@ -35948,6 +35983,12 @@ "$ref": "#/components/schemas/PasswordConfigurationResponseModel" } ] + }, + "allowChangePassword": { + "type": "boolean" + }, + "allowTwoFactor": { + "type": "boolean" } }, "additionalProperties": false @@ -35970,14 +36011,11 @@ "mediaStartNodeIds", "name", "permissions", + "userGroupIds", "userName" ], "type": "object", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, "email": { "type": "string" }, @@ -35987,6 +36025,21 @@ "name": { "type": "string" }, + "userGroupIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "id": { + "type": "string", + "format": "uuid" + }, "languageIsoCode": { "type": "string", "nullable": true @@ -37786,6 +37839,16 @@ "type": "string", "format": "date-time", "nullable": true + }, + "scheduledPublishDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "scheduledUnpublishDate": { + "type": "string", + "format": "date-time", + "nullable": true } }, "additionalProperties": false diff --git a/src/Umbraco.Cms.Api.Management/Security/BackOfficeUserManagerAuditer.cs b/src/Umbraco.Cms.Api.Management/Security/BackOfficeUserManagerAuditer.cs new file mode 100644 index 0000000000..561e67bd38 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/BackOfficeUserManagerAuditer.cs @@ -0,0 +1,142 @@ +using System.Globalization; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security; + +/// +/// Binds to notifications to write audit logs for the +/// +internal sealed class BackOfficeUserManagerAuditer : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler +{ + private readonly IAuditService _auditService; + private readonly IUserService _userService; + + public BackOfficeUserManagerAuditer(IAuditService auditService, IUserService userService) + { + _auditService = auditService; + _userService = userService; + } + + public void Handle(UserForgotPasswordChangedNotification notification) => + WriteAudit( + notification.PerformingUserId, + notification.AffectedUserId, + notification.IpAddress, + "umbraco/user/password/forgot/change", + "password forgot/change"); + + public void Handle(UserForgotPasswordRequestedNotification notification) => + WriteAudit( + notification.PerformingUserId, + notification.AffectedUserId, + notification.IpAddress, + "umbraco/user/password/forgot/request", + "password forgot/request"); + + public void Handle(UserLoginFailedNotification notification) => + WriteAudit( + notification.PerformingUserId, + null, + notification.IpAddress, + "umbraco/user/sign-in/failed", + "login failed"); + + public void Handle(UserLoginSuccessNotification notification) + => WriteAudit( + notification.PerformingUserId, + notification.AffectedUserId, + notification.IpAddress, + "umbraco/user/sign-in/login", + "login success"); + + public void Handle(UserLogoutSuccessNotification notification) + => WriteAudit( + notification.PerformingUserId, + notification.AffectedUserId, + notification.IpAddress, + "umbraco/user/sign-in/logout", + "logout success"); + + public void Handle(UserPasswordChangedNotification notification) => + WriteAudit( + notification.PerformingUserId, + notification.AffectedUserId, + notification.IpAddress, + "umbraco/user/password/change", + "password change"); + + public void Handle(UserPasswordResetNotification notification) => + WriteAudit( + notification.PerformingUserId, + notification.AffectedUserId, + notification.IpAddress, + "umbraco/user/password/reset", + "password reset"); + + private static string FormatEmail(IMembershipUser? user) => + user is null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? string.Empty : $"<{user.Email}>"; + + private void WriteAudit( + string performingId, + string? affectedId, + string ipAddress, + string eventType, + string eventDetails) + { + int? performingIdAsInt = ParseUserId(performingId); + int? affectedIdAsInt = ParseUserId(affectedId); + + WriteAudit(performingIdAsInt, affectedIdAsInt, ipAddress, eventType, eventDetails); + } + + private static int? ParseUserId(string? id) + => int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var isAsInt) ? isAsInt : null; + + private void WriteAudit( + int? performingId, + int? affectedId, + string ipAddress, + string eventType, + string eventDetails) + { + var performingDetails = "User UNKNOWN:0"; + if (performingId.HasValue) + { + IUser? performingUser = _userService.GetUserById(performingId.Value); + performingDetails = performingUser is null + ? $"User UNKNOWN:{performingId.Value}" + : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}"; + } + + var affectedDetails = "User UNKNOWN:0"; + if (affectedId.HasValue) + { + IUser? affectedUser = _userService.GetUserById(affectedId.Value); + affectedDetails = affectedUser is null + ? $"User UNKNOWN:{affectedId.Value}" + : $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}"; + } + + _auditService.Write( + performingId ?? 0, + performingDetails, + ipAddress, + DateTime.UtcNow, + affectedId ?? 0, + affectedDetails, + eventType, + eventDetails); + } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrenUserConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrenUserConfigurationResponseModel.cs index fc1d7d8b4f..e8621949d3 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrenUserConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrenUserConfigurationResponseModel.cs @@ -1,7 +1,8 @@ -using Umbraco.Cms.Api.Management.ViewModels.Security; +using Umbraco.Cms.Api.Management.ViewModels.Security; namespace Umbraco.Cms.Api.Management.ViewModels.User.Current; +// TODO (V16): Correct the spelling on this class name, it should be CurrentUserConfigurationResponseModel. public class CurrenUserConfigurationResponseModel { public bool KeepUserLoggedIn { get; set; } @@ -10,4 +11,8 @@ public class CurrenUserConfigurationResponseModel public bool UsernameIsEmail { get; set; } public required PasswordConfigurationResponseModel PasswordConfiguration { get; set; } + + public bool AllowChangePassword { get; set; } + + public bool AllowTwoFactor { get; set; } } diff --git a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs index d46a22ac44..5cbeb6cdbe 100644 --- a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs @@ -140,7 +140,9 @@ public static class DistributedCacheExtensions Id = x.Item.Id, Key = x.Item.Key, ChangeTypes = x.ChangeTypes, - Blueprint = x.Item.Blueprint + Blueprint = x.Item.Blueprint, + PublishedCultures = x.PublishedCultures?.ToArray(), + UnpublishedCultures = x.UnpublishedCultures?.ToArray() }); dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index 3063f0bd26..dda779cd0c 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -380,6 +380,10 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase /// Gets or sets a value for the maximum query string length. /// + [Obsolete("No longer used and will be removed in Umbraco 16.")] public int? MaxQueryStringLength { get; set; } /// diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs index 221a1a1af5..c5d93d979c 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs @@ -95,7 +95,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder { // entirely unpublished content does not resolve any route, but we need one i.e. for preview to work, // so we'll use the content key as path. - if (isPreview && content.IsPublished(culture) is false) + if (isPreview && _publishStatusQueryService.IsDocumentPublished(content.Key, culture ?? string.Empty) is false) { return ContentPreviewPath(content); } diff --git a/src/Umbraco.Core/DeliveryApi/ApiDocumentUrlService.cs b/src/Umbraco.Core/DeliveryApi/ApiDocumentUrlService.cs new file mode 100644 index 0000000000..f17eba49d0 --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/ApiDocumentUrlService.cs @@ -0,0 +1,34 @@ +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.DeliveryApi; + +public sealed class ApiDocumentUrlService : IApiDocumentUrlService +{ + private readonly IDocumentUrlService _documentUrlService; + + public ApiDocumentUrlService(IDocumentUrlService documentUrlService) + => _documentUrlService = documentUrlService; + + public Guid? GetDocumentKeyByRoute(string route, string? culture, bool preview) + { + // Handle the nasty logic with domain document ids in front of paths. + int? documentStartNodeId = null; + if (route.StartsWith('/') is false) + { + var index = route.IndexOf('/'); + + if (index > -1 && int.TryParse(route.Substring(0, index), out var nodeId)) + { + documentStartNodeId = nodeId; + route = route.Substring(index); + } + } + + return _documentUrlService.GetDocumentKeyByRoute( + route, + culture.NullOrWhiteSpaceAsNull(), + documentStartNodeId, + preview); + } +} diff --git a/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs b/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs index 5ee6f8d379..e5996595fc 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs @@ -13,20 +13,43 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache { private readonly IRequestPreviewService _requestPreviewService; private readonly IRequestCultureService _requestCultureService; - private readonly IDocumentUrlService _documentUrlService; + private readonly IApiDocumentUrlService _apiDocumentUrlService; private readonly IPublishedContentCache _publishedContentCache; private DeliveryApiSettings _deliveryApiSettings; + [Obsolete("Use the non-obsolete constructor. Will be removed in V17.")] public ApiPublishedContentCache( IRequestPreviewService requestPreviewService, IRequestCultureService requestCultureService, IOptionsMonitor deliveryApiSettings, IDocumentUrlService documentUrlService, IPublishedContentCache publishedContentCache) + : this(requestPreviewService, requestCultureService, deliveryApiSettings, StaticServiceProvider.Instance.GetRequiredService(), publishedContentCache) + { + } + + [Obsolete("Use the non-obsolete constructor. Will be removed in V17.")] + public ApiPublishedContentCache( + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IOptionsMonitor deliveryApiSettings, + IDocumentUrlService documentUrlService, + IApiDocumentUrlService apiDocumentUrlService, + IPublishedContentCache publishedContentCache) + : this(requestPreviewService, requestCultureService, deliveryApiSettings, apiDocumentUrlService, publishedContentCache) + { + } + + public ApiPublishedContentCache( + IRequestPreviewService requestPreviewService, + IRequestCultureService requestCultureService, + IOptionsMonitor deliveryApiSettings, + IApiDocumentUrlService apiDocumentUrlService, + IPublishedContentCache publishedContentCache) { _requestPreviewService = requestPreviewService; _requestCultureService = requestCultureService; - _documentUrlService = documentUrlService; + _apiDocumentUrlService = apiDocumentUrlService; _publishedContentCache = publishedContentCache; _deliveryApiSettings = deliveryApiSettings.CurrentValue; deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings); @@ -36,25 +59,11 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache { var isPreviewMode = _requestPreviewService.IsPreview(); - // Handle the nasty logic with domain document ids in front of paths. - int? documentStartNodeId = null; - if (route.StartsWith("/") is false) - { - var index = route.IndexOf('/'); - - if (index > -1 && int.TryParse(route.Substring(0, index), out var nodeId)) - { - documentStartNodeId = nodeId; - route = route.Substring(index); - } - } - - Guid? documentKey = _documentUrlService.GetDocumentKeyByRoute( + Guid? documentKey = _apiDocumentUrlService.GetDocumentKeyByRoute( route, _requestCultureService.GetRequestedCulture(), - documentStartNodeId, - _requestPreviewService.IsPreview() - ); + _requestPreviewService.IsPreview()); + IPublishedContent? content = documentKey.HasValue ? await _publishedContentCache.GetByIdAsync(documentKey.Value, isPreviewMode) : null; @@ -66,35 +75,11 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache { var isPreviewMode = _requestPreviewService.IsPreview(); - - // Handle the nasty logic with domain document ids in front of paths. - int? documentStartNodeId = null; - if (route.StartsWith("/") is false) - { - var index = route.IndexOf('/'); - - if (index > -1 && int.TryParse(route.Substring(0, index), out var nodeId)) - { - documentStartNodeId = nodeId; - route = route.Substring(index); - } - } - - var requestCulture = _requestCultureService.GetRequestedCulture(); - - if (requestCulture?.Trim().Length <= 0) - { - // documentUrlService does not like empty strings - // todo: align culture null vs empty string behaviour across the codebase - requestCulture = null; - } - - Guid? documentKey = _documentUrlService.GetDocumentKeyByRoute( + Guid? documentKey = _apiDocumentUrlService.GetDocumentKeyByRoute( route, - requestCulture, - documentStartNodeId, - _requestPreviewService.IsPreview() - ); + _requestCultureService.GetRequestedCulture(), + _requestPreviewService.IsPreview()); + IPublishedContent? content = documentKey.HasValue ? _publishedContentCache.GetById(isPreviewMode, documentKey.Value) : null; @@ -113,6 +98,7 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache IPublishedContent? content = _publishedContentCache.GetById(_requestPreviewService.IsPreview(), contentId); return ContentOrNullIfDisallowed(content); } + public async Task> GetByIdsAsync(IEnumerable contentIds) { var isPreviewMode = _requestPreviewService.IsPreview(); diff --git a/src/Umbraco.Core/DeliveryApi/IApiDocumentUrlService.cs b/src/Umbraco.Core/DeliveryApi/IApiDocumentUrlService.cs new file mode 100644 index 0000000000..7792fa8a47 --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/IApiDocumentUrlService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.DeliveryApi; + +public interface IApiDocumentUrlService +{ + Guid? GetDocumentKeyByRoute(string route, string? culture, bool preview); +} diff --git a/src/Umbraco.Core/DeliveryApi/ICurrentMemberClaimsProvider.cs b/src/Umbraco.Core/DeliveryApi/ICurrentMemberClaimsProvider.cs new file mode 100644 index 0000000000..cd636cb06e --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/ICurrentMemberClaimsProvider.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.DeliveryApi; + +public interface ICurrentMemberClaimsProvider +{ + /// + /// Retrieves the claims for the currently logged in member. + /// + /// + /// This is used by the OIDC user info endpoint to supply "current user" info. + /// + Task> GetClaimsAsync(); +} diff --git a/src/Umbraco.Core/DeliveryApi/IncludeInApiVersionAttribute.cs b/src/Umbraco.Core/DeliveryApi/IncludeInApiVersionAttribute.cs new file mode 100644 index 0000000000..f336126b82 --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/IncludeInApiVersionAttribute.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Cms.Core.DeliveryApi; + +[AttributeUsage(AttributeTargets.Property)] +public class IncludeInApiVersionAttribute : Attribute +{ + public int? MinVersion { get; } + + public int? MaxVersion { get; } + + /// + /// Initializes a new instance of the class. + /// Specifies that the property should be included in the API response if the API version falls within the specified bounds. + /// + /// The minimum API version (inclusive) for which the property should be included. + /// The maximum API version (inclusive) for which the property should be included. + public IncludeInApiVersionAttribute(int minVersion = -1, int maxVersion = -1) + { + MinVersion = minVersion >= 0 ? minVersion : null; + MaxVersion = maxVersion >= 0 ? maxVersion : null; + } +} diff --git a/src/Umbraco.Core/DeliveryApi/NoopCurrentMemberClaimsProvider.cs b/src/Umbraco.Core/DeliveryApi/NoopCurrentMemberClaimsProvider.cs new file mode 100644 index 0000000000..8080c27562 --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/NoopCurrentMemberClaimsProvider.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.DeliveryApi; + +public class NoopCurrentMemberClaimsProvider : ICurrentMemberClaimsProvider +{ + public Task> GetClaimsAsync() => Task.FromResult(new Dictionary()); +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 2ec4a72480..f761007146 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Media.EmbedProviders; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.ServerEvents; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Webhooks; @@ -92,6 +93,7 @@ public static partial class UmbracoBuilderExtensions builder.SortHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.ContentIndexHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.WebhookEvents().AddCms(true); + builder.ContentTypeFilters(); } /// @@ -246,4 +248,11 @@ public static partial class UmbracoBuilderExtensions /// public static ContentIndexHandlerCollectionBuilder ContentIndexHandlers(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + + /// + /// Gets the content type filters collection builder. + /// + /// The builder. + public static ContentTypeFilterCollectionBuilder ContentTypeFilters(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 78692dff98..95c6da574a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -34,6 +34,8 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.DynamicRoot; using Umbraco.Cms.Core.Preview; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.PublishedCache.Internal; using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services.FileSystem; @@ -46,6 +48,7 @@ using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; +using Umbraco.Cms.Core.Services.Filters; namespace Umbraco.Cms.Core.DependencyInjection { @@ -238,6 +241,7 @@ namespace Umbraco.Cms.Core.DependencyInjection // register published router Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); @@ -444,7 +448,6 @@ namespace Umbraco.Cms.Core.DependencyInjection // Routing Services.AddUnique(); Services.AddNotificationAsyncHandler(); - } } } diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/de_ch.xml b/src/Umbraco.Core/EmbeddedResources/Lang/de_ch.xml new file mode 100644 index 0000000000..708471fb4a --- /dev/null +++ b/src/Umbraco.Core/EmbeddedResources/Lang/de_ch.xml @@ -0,0 +1,2455 @@ + + + + The Umbraco community + https://docs.umbraco.com/umbraco-cms/extending/language-files + + + Kulturen und Hostnamen + Protokoll + Durchsuchen + Dokumenttyp ändern + Datentyp ändern + Kopieren + Neu + Exportieren + Neues Paket + Neue Gruppe + Entfernen + Deaktivieren + Inhalt bearbeiten + Einstellungen bearbeiten + Papierkorb leeren + Aktivieren + Dokumenttyp exportieren + Dokumenttyp importieren + Paket importieren + 'Canvas'-Modus starten + Abmelden + Verschieben + Benachrichtigungen + Öffentlicher Zugriff + Veröffentlichen + Veröffentlichung zurücknehmen + Aktualisieren + Erneut veröffentlichen + Entfernen + Umbenennen + Wiederherstellen + Wähle worunter kopiert werden soll + Wähle worunter verschoben werden soll + Wähle wohin importiert werden soll + Wähle wohin die ausgewählten Elemente kopiert werden soll + Wähle wohin die ausgewählten Elemente verschoben werden soll + in der Baumstrukture + wurde verschoben nach + wurde kopiert nach + wurde gelöscht + Berechtigungen + Zurücksetzen + Zur Veröffentlichung einreichen + Zur Übersetzung senden + Gruppe festlegen + Sortieren + Übersetzen + Aktualisieren + Berechtigung festlegen + Freigeben + Inhaltsvorlage anlegen + Einladung erneut versenden + + + Inhalt + Administration + Struktur + Anderes + + + Erlaube Zugriff auf "Kultur und Hostname"-Einstellungen + Erlaube Zugriff auf Bearbeiten-Verlauf + Erlaube das Anzeigen eines Knotens + Erlaube Ändern des Dokumenten-Typs + Erlaube Kopieren + Erlaube Erzeugen + Erlaube Entfernen + Erlaube Verschieben + Erlaube Zugriff auf "Öffentlich zugänglich"-Einstellungen + Erlaube Veröffentlichung + Erlaube Rücknahme der Veröffentlichung + Erlaube Zugriff auf die Berechtigungen + Erlaube Zurücksetzen auf eine vorherige Version + Erlaube Anforderungen von Veröffentlichungen + Erlaube Anfordern von Übersetzungen + Erlaube Sortieren + Erlaube Übersetzung + Erlaube Sichern von Änderungen + Erlaube Anlegen von Inhaltsvorlagen + Erlaube das Einrichten von Benachrichtungen für Inhalte + + + Inhalt + Info + + + Erlaubnis verweigert. + Neue Domain hinzufügen + Aktuelle Domain hinzufügen + entfernen + Ungültiges Element. + Format der Domain ungültig. + Domain wurde bereits zugewiesen. + Sprache + Domain + Domain '%0%' hinzugefügt + Domain '%0%' entfernt + Die Domain '%0%' ist bereits zugeordnet + Domain '%0%' aktualisiert + Domains bearbeiten + + + + Vererben + Kultur + + Definiert die Kultureinstellung für untergeordnete Elemente dieses Elements oder vererbt vom übergeordneten Element. + Wird auch auf das aktuelle Element angewendet, sofern auf tieferer Ebene keine Domain zugeordnet ist. + + Domainen + + + Auswahl aufheben + Auswählen + Etwas anderes machen + Fett + Ausrücken + Formularelement einfügen + Graphische Überschrift einfügen + HTML bearbeiten + Einrücken + Kursiv + Zentriert + Linksbündig + Rechtsbündig + Link einfügen + Anker einfügen + Aufzählung + Nummerierung + Makro einfügen + Abbildung einfügen + Veröffentlichen und schliessen + Veröffentlichen mit Unterknoten + Datenbeziehungen bearbeiten + Zurück zur Liste + Speichern + Sichern und schliessen + Speichern und veröffentlichen + Speichern und zur Abnahme übergeben + Listenansicht sichern + Veröffentlichung planen + Vorschau + Die Vorschaufunktion ist deaktiviert, da keine Vorlage zugewiesen ist + Stil auswählen + Stil anzeigen + Tabelle einfügen + Erzeuge Daten-Model und schliesse + Sichern und Daten-Model erzeugen + Zurücknehmen + Erneut anwenden + TAG entfernen + Abbrechen + Bestätigen + Mehr Veröffentlichungs Optionen + Senden + + + Medie gelöscht + Medie verschoben + Medie kopiert + Medie gesichert + + + Anzeigen als + Inhalt gelöscht + Inhalt unveröffentlicht + Inhalt unveröffentlicht für Sprache: %0% + Inhalt veröffentlicht + Inhalt veröffentlicht für Sprache: %0% + Inhalt gesichert + Inhalt gesichert für Sprache: %0% + Inhalt verschoben + Inhalt kopiert + Inhalt auf vorherige Version geändert + Veröffentlichung für Inhalt angefordert + Veröffentlichung für Inhalt angefordert in Sprache: %0% + Unterknoten wurden sortiert von Benutzer + %0% + Versionsbereinigung deaktiviert für Version: %0% + Versionsbereinigung aktiviert für Version: %0% + Kopieren + Veröffentlichen + Veröffentlichen + Verschieben + Sichern + Sichern + Entfernen + Veröffentlichung zurücknehmen + Veröffentlichung zurücknehmen + Vorgängerversion wieder herstellen + Veröffentlichung anfordern + Veröffentlichung anfordern + Sortieren + Benutzerdefiniert + Speichern + Speichern + Verlauf (alle Variationen) + + + Der Verzeichnisname darf keine ungültigen Zeichen enthalten. + Folgendes Element konnte nicht entfernt werden: %0% + + + Ist veröffentlicht + Über dieses Dokument + Alias + (Wie würden Sie das Bild über das Telefon beschreiben?) + Alternative Links + Klicken, um das Dokument zu bearbeiten + Erstellt von + Ursprünglicher Autor + Aktualisiert von + Erstellt am + Erstellungszeitpunkt des Dokuments + Dokumenttyp + In Bearbeitung + Veröffentlichung aufheben am + Dieses Dokument wurde nach dem Veröffentlichen bearbeitet. + Dieses Dokument ist nicht veröffentlicht. + Zuletzt veröffentlicht + Keine Elemente anzuzeigen + Diese Liste enthält keine Einträge. + Es wurden keine untergeordneten Elemente hinzugefügt + Es wurden keine Mitglieder hinzugefügt + Medientyp + Verweis auf Medienobjekt(e) + Mitgliedergruppe + Mitgliederrolle + Mitglieder-Typ + Es wurden keine Änderungen vorgenommen + Kein Datum gewählt + Name des Dokument + Dieses Media-Element hat keinen Link + Diesem Element kann kein Inhalt zugewiesen werden + Eigenschaften + + Dieses Dokument ist veröffentlicht aber nicht sichtbar, + da das übergeordnete Dokument '%0%' nicht publiziert ist + + + Diese Kultur wurde veröffentlicht, aber wird nicht angezeigt, + weil sie auf dem Oberknoten '%0%' unveröffentlicht ist + + Ups! Dieses Dokument ist veröffentlicht aber nicht im internen Cache aufzufinden: Systemfehler. + Der URL wurde nicht gefunden + Dieses Dokument wurde veröffentlicht, aber sein URL würde mit Inhalt %0% kollidieren + Dieses Dokument wurde veröffentlicht, aber sein URL kann nicht aufgelöst (routed) werden + Veröffentlichen + Veröffentlicht + Veröffentlicht (Änderungen bereit) + Publikationsstatus + + Veröffentlichen mit Unterknoten zum Veröffentlichen der gewählten Sprache samt aller Unterknoten der selben Sprache, um ihren Inhalt öffentlich verfügbar zu machen.]]> + + + Veröffentlichen mit Unterknoten zum Veröffentlichen der gewählten Sprache samt aller Unterknoten der selben Sprache, um ihren Inhalt öffentlich verfügbar zu machen.]]> + + Veröffentlichen am + Veröffentlichung widerrufen am + Datum entfernen + Datum wählen + Sortierung abgeschlossen + + Um die Dokumente zu sortieren, ziehen Sie sie einfach an die gewünschte Position. + Sie können mehrere Zeilen markieren indem Sie die Umschalttaste ("Shift") oder die Steuerungstaste ("Strg") gedrückt halten + + Statistiken + Titel (optional) + Alternativtext (optional) + Beschriftung (optional) + Typ + Veröffentlichung widerrufen + Entwurf + Nicht angelegt + Zuletzt bearbeitet am + Letzter Änderungszeitpunkt des Dokuments + Datei entfernen + Klicke hier um das das Bild vom Medienelement zu entfernen. + Klicke hier um das das Bild vom Medienelement zu entfernen. + Link zum Dokument + Mitglied der Gruppe(n) + Kein Mitglied der Gruppe(n) + Untergeordnete Elemente + Ziel + Dies führt zur folgenden Zeit auf dem Server: + + Was bedeutet dies?]]> + + Wollen Sie dieses Element wirklich entfernen? + Sicher das Sie alle Elemente entfernen wollen? + + Eigenschaft %0% verwendet Editor %1%, + welcher nicht von Nested Content unterstützt wird. + + Keine Dokument-Typen für diese Eigenschaft konfiguriert. + Elementtyp hinzufügen + Elementtype auswählen + + Wählen Sie die Gruppe aus von der die Eigenschaften angezeigt werden soll. Sollte der Wert leer sein + wird die erste Gruppe des Elementtypen verwendet. + + + Geben Sie eine Angular Anweisung an um den Namen für das jweilige Element + zu bestimmen. Verwende + + um den Index des Elements anzuzeigen. + Das ausgewählte Element verfügt nicht über unterstützte Gruppen. (Tabs werden von diesem Editor nicht unterstützt, entweder Sie ändern diese zu Gruppen oder Sie verwenden den Block List Editor. + Füge ein weiteres Textfeld hinzu + Entferne dieses Textfeld + Inhalt-Basis + Inklusive Entwürfen: veröffentliche auch unveröffentlichte Elemente. + + Dieser Wert ist verborgen. + Wenn Sie diesen Wert einsehen müssen, wenden Sie sich bitte an einen Administrator. + + Dieser Wert ist verborgen. + Welche Sprache möchten Sie veröffentlichen? + Welche Sprachen möchten Sie zur Freigabe schicken? + Welche Sprachen möchten Sie zu einer bestimmten Zeit veröffentlichen? + + Wählen Sie die Sprachen, deren Veröffentlichung zurück genommen werden soll. + Das Zurücknehmen der Veröffentlichung einer Pflichtsprache betrifft alle Sprachen. + + Alle neuen Variationen werden gespeichert. + Welche Variationen wollen Sie veröffentlichen? + Wählen Sie welche Variation gespeichert werden soll. + Folgende Variationen werden benötigt um das Element veröffentlichen zu können: + Wir sind für Veröffentlichungen bereit + Bereit zu Veröffentlichen? + Bereit zu Sichern? + Fokus zurücksetzten. + Freigabe anfordern + Wählen Sie Datum und Uhrzeit für die Veröffentlichung bzw. deren Rücknahme. + Neues Element anlegen + Aus der Zwischenablage einfügen + Dieses Element befindet sich im Papierkorb. + Speichern ist nicht erlaubt. + Veröffentlichen ist nicht erlaubt. + Zur Genehmigung senden ist nicht erlaubt. + Plannung ist nicht erlaubt + Veröffentlichung zurücknehmen ist nicht erlaubt. + + + %0%]]> + Leer + Wählen Sie eine Inhaltsvorlage + Inhaltsvorlage erzeugt + Inhaltsvorlage von '%0%' wurde erzeugt + Eine gleichnamige Inhaltsvorlage ist bereits vorhanden + + Eine Inhaltsvorlage ist vordefinierter Inhalt, + den ein Redakteur als Basis für neuen Inhalt verwenden kann + + + + Für Upload klicken + oder klicken Sie hier um eine Datei zu wählen + Dieser Dateityp darf nicht hochgeladen werden + + Diese Datei kann nicht hochgeladen werden wil der Dateiname ungültig ist. + Diese Datei kann nicht hochgeladen werden, der Medienttype mit dem Alias '%0%' ist hier nicht erlaubt. + Max. Dateigröße ist + Media-Basis + Eltern- und Ziel-Verzeichnis dürfen nicht übereinstimmen + Unter Element Id %0% konnte kein Verzeichnis angelegt werden + Das Verzeichnis mit Id %0% konnte nicht umbenannt werden + Wählen Sie Dateien aus und ziehen Sie diese in diesen Bereich + Hochladen ist in diesem Bereich nicht erlaubt. + + + Neues Mitglied anlegen + Alle Mitglieder + Ein Mitglied mit diesem Login existiert bereits. + Mitgliedsgruppen haben keine weiteren editierbaren Eigenschaften. + Das Mitglied ist bereits in der Gruppe '%0%' + Das Mitglied hat bereits ein Passwort + Sperren ist nicht aktiviert für dieses Mitglied. + Das Mitglied ist nicht in der Gruppe '%0%' + Zwei-Faktor-Authentifizierung + + + Kopieren des Dokumenttyps fehlgeschlagen + Bewegen des Dokumenttyps fehlgeschlagen + + + Kopieren des Medienttyps fehlgeschlagen + Bewegen des Medienttyps fehlgeschlagen + Automatische auswahl. + + + Kopieren des Mitgliedtyps fehlgeschlagen + + + An welcher Stellen wollen Sie das Element erstellen + Neues Element unterhalb von + Wählen Sie einen Dokumenttyp für eine Inhaltsvorlage + Geben Sie einen Verzeichnisnamen ein + Wählen Sie einen Namen und einen Typ + + + + + + + + Die im Inhaltsbaum ausgewählte Seite + erlaubt keine Unterseiten. + + Bearbeitungsrechte für diesen Dokumenttyp + Neuen Dokumenttypen erstellen + + + + + + + + Das im Strukturbaum ausgewählte Medienelement + erlaubt keine untergeordneten Elemente. + + Bearbeitungsrechte für diesen Medientyp + Dokumenttyp ohne Vorlage + Dokumenttyp mit Template + + Die Definition für eine Inhaltsseite welche von einem + Redakteur im Inhaltsbaum angelgt werden können und direkt über die URL aufgerufen werden kann. + + Dokumenttype + + Die Definition für eine Inhaltsseite welche von einem + Redakteur im Inhaltsbaum angelgt werden können und direkt über die URL aufgerufen werden kann. + + Elementtyp + + Definiert die Vorlage für sich wiederholende Eigenschaften, zum Beispiel, in einer 'Block + List' oder im 'Nested Content' Editor. + + Komposition + + Definiert eine wiederverwendbare Komposition von Eigenschaften welche in anderen + Dokumenttypen wiederverwendet werden können. + + Ordner + + Werden benützt um Dokumenttypen, Komposition und Elementtypen in diesem + Dokumenttypbaums zu organisieren. + + Neues Verzeichnis + Neuer Datentyp + Neue JavaScript-Datei + Neue leere Partial-View + Neues Partial-View-Makro + Neue Partial-View nach Vorlage + Neues Partial-View-Makro nach Vorlage + Neues Partial-View-Makro (ohne Makro) + Neue Style-Sheet-Datei + Neue Rich-Text-Editor Style-Sheet-Datei + + + Website anzeigen + - Verstecken + Wenn Umbraco nicht geöffnet wurde, wurde möglicherweise das Pop-Up unterdrückt. + wurde in einem neuen Fenster geöffnet + Neu öffnen + Besuchen + Willkommen + + + Bleiben + Änderungen verwerfen + Es gibt ungesicherte Änderungen + + Wollen Sie diese Seite wirklich verlassen? + - es gibt ungesicherte Änderungen + + Veröffentlichen macht die ausgewählten Elemente auf der Website sichtbar. + + Aufheben der Veröffentlichung entfernt die ausgewählten Elemente + und ihre Unterknoten von der Website. + + Aufheben der Veröffentlichung entfernt diese Seite und ihre Unterseiten von der Website. + + Es gibt ungesicherte Änderungen. + Ändern des Dokumenttyps macht diese rückgängig. + + + + Fertig + %0% Element entfernt + %0% Elemente entfernt + %0% von %1% Element entfernt + %0% von %1% Elementen entfernt + %0% Element veröffentlicht + %0% Elemente veröffentlicht + %0% von %1% Element veröffentlicht + %0% von %1% Elementen veröffentlicht + %0% Veröffentlichung aufgehoben + %0% Veröffentlichungen aufgehoben + %0% von %1% Veröffentlichung aufgehoben + %0% von %1% Veröffentlichungen aufgehoben + %0% Element verschoben + %0% Elemente verschoben + %0% von %1% Element verschoben + %0% von %1% Elementen verschoben + %0% Element kopiert + %0% Elemente kopiert + %0% von %1% Element kopiert + %0% von %1% Elementen kopiert + + + Name des Link + Link + Anker / querystring + Name + Fenster schließen + Wollen Sie dies wirklich entfernen + %0% von %1% Elementen löschen wollen?]]> + Wollen Sie folgendes wirklich deaktivieren + Sicher das Sie es entfernen wollen? + %0% entfernen wollen?]]> + Sind Sie sich wirklich abmelden? + Sind Sie sicher? + Ausschneiden + Wörterbucheintrag bearbeiten + Sprache bearbeiten + Ausgewähltes Medien + Anker einfügen + Zeichen einfügen + Grafische Überschrift einfügen + Abbildung einfügen + Link einfügen + klicken um Macro hinzuzufügen + Tabelle einfügen + Dies entfernt die Sprache + + Die Kultur-Variante einer Sprache zu ändern ist möglicherweise eine aufwendige Operation und führt zum Erneuern von Inhalts-Zwischenspeicher und Such-Index. + + Zuletzt bearbeitet + Verknüpfung + Anker: + Wenn lokale Links verwendet werden, füge ein "#" vor den Link ein + In einem neuen Fenster öffnen? + Dieses Makro enthält keine einstellbaren Eigenschaften. + Einfügen + Berechtigungen bearbeiten für + Berechtigungen vergeben für + Berechtigungen vergeben für %0% für Benutzer-Gruppe %1% + Wählen Sie die Benutzer-Gruppe, deren Berechtigungen Sie setzen möchten + + Der Papierkorb wird geleert. + Bitte warten Sie und schließen Sie das Fenster erst, wenn der Vorgang abgeschlossen ist. + + Der Papierkorb ist leer + Wenn Sie den Papierkorb leeren, werden die enthaltenen Elemente endgültig gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden. + + Der Webservice von <a target='_blank' rel='noopener' href='http://regexlib.com'>regexlib.com</a> ist zur Zeit nicht erreichbar. Bitte versuchen Sie es später erneut. + + + Finden Sie einen vorbereiteten regulären Ausdruck zur Validierung der Werte, die in dieses Feld eingegeben werden - zum Beispiel 'email, 'plz', 'URL' oder ähnlich. + + Macro entfernen + Pflichtfeld + Die Website-Index wurd neu erstellt + + Der Zwischenspeicher der Website wurde aktualisiert und alle veröffentlichten Inhalte sind jetzt auf dem neuesten Stand. + Bisher unveröffentliche Inhalte wurden dabei nicht veröffentlicht. + + + Der Zwischenspeicher der Website wird aktualisiert und der veröffentlichte Inhalt auf den neuesten Stand gebracht. + Unveröffentlichte Inhalte bleiben dabei weiterhin unveröffentlicht. + + Anzahl der Spalten + Anzahl der Zeilen + Für Originalgröße auf die Abbildung klicken + Element auswählen + Zwischenspeicher-Element anzeigen + Verknüpfe mit Original + Einschliesslich Unterknoten + Die freundlichste Community + Seiten-Link + In neuem Fenster / Tab öffnen + Medien-Link + Inhalts-Startknoten wählen + Medienelement wählen + Medientype wählen + Bildzeichen wählen + Element wählen + Link wählen + Makro wählen + Inhalt wählen + Inhaltstyp wählen + Medien-Startknoten wählen + Mitglied wählen + Mitgliedergruppe wählen + Membertype wählen + Knoten wählen + Bereich wählen + Sprachen wählen + Benutzer wählen + Benutzer wählen + Keine Bildzeichen gefunden + Für dieses Makro gibt es keine Parameter + Es gibt keine Makros zum Einfügen + Externe Login-Anbieter + Ausnahmedetails + Stacktrace + Inner Exception + Verknüpfen Sie Ihr + Trennen Sie Ihr + Konto + Editor wählen + Konfiguration wählen + Kode-Vorlage wählen + + Dies wird den Knoten und all seine Sprachen entfernen. + Wenn Sie nur eine Sprache entfernen wollen, wählen Sie diese und setzen sie auf unveröffentlicht. + + %0%.]]> + %0% von der %1% Gruppe]]> + Ja, entfernen + Sie löschen das Layout + Bearbeiten des layout resultiert im Verlust der aller Daten für bestehenden Inhalt basierend auf dieser Konfiruation. + + + + Um einen Eintrag zu importieren, suchen sie die ".udt" Datei auf ihren Computer durch Klicken des + "Import" Knopfes. (Sie werden für Ihre Zustimmung im nächsten Schritt gefragt) + + Wörterbuch Eintrag existiert nicht. + Eltern Element existiert nicht. + Es gibt keine Einträge im Wörterbuch. + Keine Wörterbuch Einträge in dieser Datei gefunden. + Keine Wörterbuch Einträge gefunden. + Eintrag erstellen + + + + %0%'. +
Unter dem links angezeigten Menüpunkt 'Sprachen' können Sie weitere hinzufügen.]]> +
+ Name der Kultur + + + + Wörterbuch Übersicht + + + Sucher einrichten ]]> + + Sucher (z.B.: multi-index searcher)]]> + + Feldwerte + Gesundheitsstatus + Der Gesundheitsstatus und Lesbarkeit des Indizes. + Indizierer + Indexinformationen + Inhalt des Indexes + Zeigt die Eigenschaften des Indizes + Examine Index-Verwaltung + + Index Detailanzeige und Verwaltungswerkzeuge + + Index erneuern + + + Abhängig von der Inhaltsmenge Ihrer Website kann das eingie Zeit dauern.
+ Es wird davon abgeraten, einen Index einer Website während hoher Auslastung- oder Inhaltbearbeitungszeiten zu erneuern. + ]]> +
+ Sucher + Durchsuche den Index und betrachte die Ergebnisse + Werkzeuge + Werkzeuge zur Indexverwaltung + Felder + Der Index kann nicht gelesen werden und wird deshalb neu erstellt. + + Der Prozess dauert länger als erwartet, checken Sie die Umbraco Logs um zu sehen ob + Fehler passiert sind. + + Der Index kann nicht rebuilded werden weil er nicht zugewissen wurde. + IIndexPopulator + + + Benutzername eingeben + Kennwort eingeben + Bestätige das Kennwort + %0% benennen ... + Bitte Name angeben ... + Bitte E-Mail eingeben... + Bitte Benutzernamen eingeben... + Label... + Bitte eine Beschreibung eingeben... + Durchsuchen ... + Filtern ... + Tippen, um Tags hinzuzufügen (nach jedem Tag die Eingabetaste drücken) ... + Bitte E-Mail eingeben + Bitte Nachricht eingeben... + Der Benutzername ist normalerweise Ihre E-Mail-Adresse + #value oder ?key=value + Bitte einen Alias eingeben... + Alias erzeugen... + Element erstellen + Bearbeiten + Benennen + + + Angepasste Listenansicht erstellen + Angepasste Listenansicht entfernen + Ein Inhalts-, Medien oder Mitgliedstyp mit gleichem Alias ist bereits vorhanden. + + + Umbenannt + Tragen Sie hier einen neuen Verzeichnisnamen ein + %0% wurde umbenannt in %1% + + + Ändern des Editors in einem Datatyps mit gespeicherten Werten ist nicht erlaubt. Um es zu erlauben müssen Sie die Umbraco:CMS:DataTypes:CanBeChanged Einstellung in der appsettings.json ändern. + Neuer Vorgabewert + Feldtyp in der Datenbank + Datentyp-GUID + Steuerelement zur Darstellung + Schaltflächen + Erweiterte Einstellungen aktivieren für + Kontextmenü aktivieren + Maximale Standardgröße für eingefügte Bilder + Verknüpfte Stylesheets + Beschriftung anzeigen + Breite und Höhe + Wählen Sie das Verzeichnis aus der untenstehenden Baumstruktur, in das + verschoben werden soll. + wurde verschoben in + + %0% wird die Eigenschaften und Daten von folgenden Element löschen]]> + + + Ich verstehe das diese Aktion Eigenschaften und Daten basierend auf diesem + DataTyps löschen wird. + + + + + Ihre Daten wurden gespeichert. + Bevor Sie diese Seite jedoch veröffentlichen können, müssen Sie die folgenden Korrekturen vornehmen: + + + Der aktuelle Mitgliedschaftsanbieter erlaubt keine Kennwortänderung + (EnablePasswordRetrieval muss auf "true" gesetzt sein) + + '%0%' ist bereits vorhanden + Bitte prüfen und korrigieren: + Bitte prüfen und korrigieren: + + Für das Kennwort ist eine Mindestlänge von %0% Zeichen vorgesehen, + wovon mindestens %1% Sonderzeichen (nicht alphanumerisch) sein müssen + + '%0%' muss eine Zahl sein + '%0%' (in Registerkarte '%1%') ist ein Pflichtfeld + '%0%' ist ein Pflichtfeld + '%0%' (in Registerkarte '%1%') hat ein falsches Format + '%0%' hat ein falsches Format + + + Ein unbekannter Fehler ist passiert. + Optimistic concurrency Fehler, Objekte wurde geändert. + Der Server hat einen Fehler gemeldet + Dieser Dateityp wird durch die Systemeinstellungen blockiert + + ACHTUNG! Obwohl CodeMirror in den Einstellungen aktiviert ist, + bleibt das Modul wegen mangelnder Stabilität in Internet Explorer deaktiviert. + + Bitte geben Sie die Bezeichnung und den Alias des neuen Dokumenttyps ein. + Es besteht ein Problem mit den Lese-/Schreibrechten auf eine Datei oder einen Ordner + Fehler beim Laden einer "Partial View Kodedatei" (Datei: %0%) + Bitte geben Sie einen Titel ein + Bitte wählen Sie einen Typ + + Soll die Abbildung wirklich über die + Originalgröße hinaus vergrößert werden? + + Startelement gelöscht, bitte kontaktieren Sie den System-Administrator. + Bitte markieren Sie den gewünschten Text, bevor Sie einen Stil auswählen + Keine aktiven Stile vorhanden + Bitte platzieren Sie den Mauszeiger in die erste der zusammenzuführenden Zellen + Sie können keine Zelle trennen, die nicht zuvor aus mehreren zusammengeführt wurde. + Die Eigenschaft ist nicht valide + + + Optionen + Info + Aktion + Aktionen + Hinzufügen + Alias + Alles + Sind Sie sicher? + Zurück + Zurück zur Übersicht + Rahmen + von + Abbrechen + Zellabstand + Auswählen + Schließen + Leeren + Fenster schließen + Fenster Pane + Kommentar + bestätigen + Beschneiden + Seitenverhältnis beibehalten + Inhalt + Weiter + Kopieren + Neu + Ausschnitte Bereich + Datenbank + Datum + Standard + Löschen + Gelöscht + Löschen ... + Design + Wörterbuch + Abmessungen + Verwerfen + nach unten + Herunterladen + Bearbeiten + Bearbeitet + Elemente + E-Mail + Fehler + Feld + Suche + Erste(s) + Allgemein + Gruppen + Gruppe + Generisch + Gruppe + Höhe + Hilfe + Verbergen + Verlauf + Bildzeichen + Id + Import + Nur in diesem Ordner suchen + Info + Innerer Abstand + Einfügen + Installieren + Ungültig + Zentrieren + Bezeichnung + Sprache + Letzte(s) + Layout + Links + Laden + Gesperrt + Anmelden + Abmelden + Abmelden + Makro + Pflichtfeld + Medien + Nachricht + Verschieben + Name + Neu + Weiter + Nein + Knoten Name + von + Aus + Ok + Öffnen + An + oder + Sortieren nach + Kennwort + Pfad + Einen Moment bitte... + Zurück + Eigenschaften + Mehr erfahren + Erneuern + E-Mail-Empfänger für die Formulardaten + Papierkorb + Ihr Mülleimer ist leer + Neu laden + Verbleibend + Entfernen + Rückgängig + Umbenennen + Erneuern + Pflichtangabe + Wiederherstellen + Wiederholen + Berechtigungen + Geplantes Veröffentlichen + Umbraco Information + Suchen + Leider können wir nicht finden, wonach Sie suchen. + Es wurden keine Elemente hinzugefügt + Server + Einstellungen + Geteilt + Anzeigen + Seite beim Senden anzeigen + Größe + Sortieren + Status + Senden + Erfolt + Typ + Typ Name + Durchsuchen ... + unter + nach oben + Aktualisieren + Update + Hochladen + URL + Benutzer + Benutzername + Validieren + Wert + Ansicht + Willkommen ... + Breite + Ja + Ordner + Suchergebnisse + Sortieren + Sortierung abschließen + Vorschau + Kennwort ändern + nach + Listenansicht + Sichern läuft... + Aktuelle(s) + Eingebettet + ausgewählt + Anderes + Artikel + Videos + Avatar für + Kopf + System Feld + Zuletzt geändert + + + Blau + + + Tab hinzufügen + Neue Gruppe + Neue Eigenschaft + Editor hinzufügen + Vorlage hinzufügen + Knoten unterhalb hinzufügen + Element unterhalb hinzufügen + Datentyp bearbeiten + Bereiche wechseln + Abkürzungen + Abkürzungen anzeigen + Listenansicht wechseln + Wurzelknotenberechtigung wechseln + Zeile ein-/auskommentieren + Zeile entfernen + Zeilen oberhalb kopieren + Zeilen unterhalb kopieren + Zeilen nach oben schieben + Zeilen nach unten schieben + Standard + Editor + Kulturvariantenberechtigung wechseln + + + Hintergrundfarbe + Fett + Textfarbe + Schriftart + Text + + + Dokument + + + Mit dieser Datenbank kann leider keine Verbindung hergestellt werden. + + Appsettings.json Datei konnte nicht gespeichert werden. Bitte ändern Sie die Datei + manuell. + + Die Datenbank ist erreichbar und wurde identifiziert als + Datenbank + + Installieren, um die Datenbank für Umbraco %0% einzurichten. + ]]> + + + Die Datenbank wurde für Umbraco %0% konfiguriert. + Klicken Sie auf <strong>weiter</strong>, um fortzufahren. + + Um diesen Schritt abzuschließen, müssen Sie die notwendigen Informationen zur Datenbankverbindung angeben.<br />Bitte kontaktieren Sie Ihren Provider bzw. Server-Administrator für weitere Informationen. + + + Bitte bestätigen Sie mit einem Klick auf Update, dass die Datenbank auf Umbraco %0% aktualisiert werden soll. +

+

+ Keine Sorge - Dabei werden keine Inhalte gelöscht und alles wird weiterhin funktionieren! +

+ ]]> +
+ Die Datenbank wurde auf die Version %0% aktualisiert. Klicken Sie auf <strong>weiter</strong>, um fortzufahren. + Die Datenbank ist fertig eingerichtet. Klicken Sie auf <strong>"weiter"</strong>, um mit der Einrichtung fortzufahren. + <strong>Das Kennwort des Standard-Benutzers muss geändert werden!</strong> + <strong>Der Standard-Benutzer wurde deaktiviert oder hat keinen Zugriff auf Umbraco.</strong></p><p>Es sind keine weiteren Aktionen notwendig. Klicken Sie auf <b>Weiter</b> um fortzufahren. + <strong>Das Kennwort des Standard-Benutzers wurde seit der Installation verändert.</strong></p><p>Es sind keine weiteren Aktionen notwendig. Klicken Sie auf <b>Weiter</b> um fortzufahren. + Das Kennwort wurde geändert! + Schauen Sie sich die Einführungsvideos für einen schnellen und einfachen Start an. + Noch nicht installiert. + Betroffene Verzeichnisse und Dateien + Weitere Informationen zum Thema "Dateiberechtigungen" für Umbraco + Für die folgenden Dateien und Verzeichnisse müssen ASP.NET-Schreibberechtigungen gesetzt werden + <strong>Die Dateiberechtigungen sind fast perfekt eingestellt!</strong><br /><br />Damit können Sie Umbraco ohne Probleme verwenden, werden aber viele Erweiterungspakete können nicht installiert werden. + Problemlösung + Klicken Sie hier, um den technischen Artikel zu lesen + Schauen Sie sich die <strong>Video-Lehrgänge</strong> zum Thema Verzeichnisberechtigungen für Umbraco an oder lesen Sie den technischen Artikel. + <strong>Die Dateiberechtigungen sind möglicherweise fehlerhaft!</strong>Sie können Umbraco vermutlich ohne Probleme verwenden, werden aber viele Erweiterungspakete können nicht installiert werden. + + <strong>Die Dateiberechtigungen sind nicht geeignet!</strong><br /><br /> + Die Dateiberechtigungen müssen angepasst werden. + + <strong>Die Dateiberechtigungen sind perfekt eingestellt!</strong><br /><br /> Damit ist Umbraco komplett eingerichtet und es können problemlos Erweiterungspakete installiert werden. + Verzeichnisprobleme lösen + Folgen Sie diesem Link für weitere Informationen zum Thema ASP.NET und der Erstellung von Verzeichnissen. + Verzeichnisberechtigungen anpassen + Umbraco benötigt Schreibrechte auf verschiedene Verzeichnisse, um Dateien wie Bilder oder PDF-Dokumente speichern zu können. Außerdem werden temporäre Daten zur Leistungssteigerung der Website angelegt. + Ich möchte mit einem leeren System ohne Inhalte und Vorgaben starten + + Die Website ist zur Zeit komplett leer und ohne Inhalte und Vorgaben zu Erstellung eigener Dokumenttypen und Vorlagen bereit. + (<a href="https://umbraco.tv/documentation/videos/for-site-builders/foundation/document-types">So geht's</a>) + Sie können "Runway" auch jederzeit später installieren. Verwenden Sie hierzu den Punkt "Pakete" im Entwickler-Bereich. + + Die Einrichtung von Umbraco ist abgeschlossen und das Content-Management-System steht bereit. Wie soll es weitergehen? + 'Runway' wurde installiert + + Die Basis ist eingerichtet. Wählen Sie die Module aus, die Sie nun installieren möchten.<br /> + Dies sind unsere empfohlenen Module. Schauen Sie sich die an, die Sie installieren möchten oder Sie sich die <a href="#" onclick="toggleModules(); return false;" id="toggleModuleList">komplette Liste der Module an.</a> + + Nur für erfahrene Benutzer empfohlen + Ich möchte mit einer einfache Website starten + + <p> + "Runway" ist eine einfache Website mit einfachen Dokumententypen und Vorlagen. Der Installer kann Runway automatisch einrichten, + aber es kann einfach verändert, erweitert oder entfernt werden. Es ist nicht zwingend notwendig und Umbraco kann auch ohne Runway verwendet werden. + Runway bietet eine einfache Basis zum schnellen Start mit Umbraco. + Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die "Runway Modules" und Ihre Runway-Seite erweitern. + </p> + <small> + <em>Runway umfasst:</em> Home page, Getting Started page, Installing Modules page.<br /> + <em>Optionale Module:</em> Top Navigation, Sitemap, Contact, Gallery. + </small> + + Was ist 'Runway'? + Schritt 1/5 Lizenz + Schritt 2/5: Datenbank + Schritt 3/5: Dateiberechtigungen + Schritt 4/5: Sicherheit + Schritt 5/5: Umbraco ist startklar! + Vielen Dank, dass Sie Umbraco installieren! + <h3>Zur neuen Seite</h3>Sie haben Runway installiert, schauen Sie sich doch mal auf Ihrer Website um. + <h3>Weitere Hilfe und Informationen</h3>Hilfe von unserer preisgekrönten Community, Dokumentation und kostenfreie Videos, wie Sie eine einfache Website erstellen, ein Packages nutzen und eine schnelle Einführung in alle Umbraco-Begriffe + Umbraco %0% wurde installiert und kann verwendet werden + Sie können <strong>sofort starten</strong>, in dem Sie auf "Umbraco starten" klicken. + <h3>Umbraco starten</h3>Um Ihre Website zu verwalten, öffnen Sie einfach den Administrationsbereich und beginnen Sie damit, Inhalte hinzuzufügen sowie Vorlagen und Stylesheets zu bearbeiten oder neue Funktionen einzurichten + Verbindung zur Datenbank fehlgeschlagen. + Umbraco Version 3 + Umbraco Version 4 + Anschauen + Dieser Assistent führt Sie durch die Einrichtung einer neuen Installation von <strong>Umbraco %0%</strong> oder einem Upgrade von Version 3.0.<br /><br />Klicken Sie auf <strong>weiter</strong>, um zu beginnen. + + + Kode der Kultur + Name der Kultur + + + Sie haben keine Tätigkeiten mehr durchgeführt und werden automatisch abgemeldet in + Erneuern Sie, um Ihre Arbeit zu speichern ... + + + Willkommen + Willkommen + Willkommen + Willkommen + Willkommen + Willkommen + Willkommen + Hier anmelden: + Anmelden mit + Sitzung abgelaufen + <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="https://umbraco.com" style="text-decoration: none" target="_blank" rel="noopener">umbraco.org</a></p> + Kennwort vergessen? + Es wird eine E-Mail mit einem Kennwort-Zurücksetzen-Link an die angegebene Adresse geschickt. + Es wird eine E-Mail mit Anweisungen zum Zurücksetzen des Kennwortes an die angegebene Adresse geschickt sofern diese im Datenbestand gefunden wurde. + Kennwort zeigen + Kennwort verbergen + Zurück zur Anmeldung + Bitte wählen Sie ein neues Kennwort + Ihr Kennwort wurde aktualisiert + Der aufgerufene Link ist ungültig oder abgelaufen + Umbraco: Kennwort zurücksetzen + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Das Zurücksetzen Ihres Kennwortes wurde angefordert +

+

+ Ihr Benutzername für das Umbraco-Administration lautet: %0% +

+

+ + + + + + +
+ + Klicken Sie hier, um Ihr Kennwort zurück zu setzen + +
+

+

Wenn Sie den Link nicht klicken können, kopieren Sie den fogenden URL und fügen Sie ihn direkt im Browser-Fenster ein:

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]> +
+ + + Dashboard + Bereiche + Inhalt + + + Bitte Element auswählen ... + %0% wurde nach %1% kopiert + Bitte wählen Sie, wohin das Element %0% kopiert werden soll: + %0% wurde nach %1% verschoben + Bitte wählen Sie, wohin das Element %0% verschoben werden soll: + wurde als das Ziel ausgewählt. Bestätigen mit 'Ok'. + Es ist noch kein Element ausgewählt. Bitte wählen Sie ein Element aus der Liste aus, bevor Sie fortfahren. + Das aktuelle Element kann aufgrund seines Dokumenttyps nicht an diese Stelle verschoben werden. + Das ausgewählte Element kann nicht zu einem seiner eigenen Unterelemente verschoben werden. + Dieses Element kann nicht auf der obersten Ebene platziert werden. + Diese Aktion ist nicht erlaubt, da Sie unzureichende Berechtigungen für mindestens ein untergeordnetes Element haben. + Kopierte Elemente mit dem Original verknüpfen + + + Bearbeiten Sie Ihre Benachrichtigungseinstellungen für '%0%' + Benachrichtigungseinstellungen wurden gesichert für + + Hallo %0%, + + die Aufgabe '%1%' (von Benutzer '%3%') an der Seite '%2%' wurde ausgeführt. + + Zum Bearbeiten verwenden Sie bitte diesen Link: http://%4%/#/content/content/edit/%5% + + Einen schönen Tag wünscht + Ihr freundlicher Umbraco-Robot + + Die folgenden Sprachen wurden geändert %0% + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hallo %0%, +

+

+ Diese automatische E-Mail soll Sie informiern, dass die Aufgabe '%1%' auf Seite '%2%' von Benutzer '%3%' ausgeführt wurde. +

+ + + + + + +
+ +
+ Bearbeiten
+
+

+

Zusammenfassung der Änderungen:

+ %6% +

+

+ Einen schönen Tag wünscht
+
+ Ihr freundlicher Umbraco-Robot +

+
+
+


+
+
+ + +]]> +
+ + Folgende Sprachen wurden geändert:

+ %0% + ]]> +
+ [%0%] Benachrichtigung: %1% ausgeführt an Seite '%2%' + Benachrichtigungen + + + Aktionen + Angelegt + Neues Paket + + + Umbraco-Pakete besitzen üblicherweise die Dateiendungen ".umb" oder ".zip". + ]]> + + Diese Aktion entfernt das Paket + Alle Unterknoten einschließen + Installiert + Installierte Pakete + Diese Paket hat keine Einstellungen + Es wurden noche keine Pakete angelegt + Sie haben keine Pakete installiert + + 'Pakete' rechts, oben), um es zu installieren + ]]> + + Paketinhalt + Lizenz + Paket suchen + Ergebnis(se) für + Keine Ergebnisse für + Bitte versuchen Sie einen anderen Begriff oder stöbern Sie in den Kategorien + Beliebt + Neue Veröffentlichungen + hat + Karma Punkte + Information + Besitzer + Beitragende + Angelegt + Aktuelle Version + .NET Version + Heruntergeladenes + Likes + Kompatibilität + + Dieses Paket ist nach Berichten von Community-Mitgliedern mit folgenden Umbraco-Version kompatibel. + Es kann keine vollständige Kompatibilität garantiert werden für Versionen mit weniger als 100% Bewertungen. + + Externe Quellen + Autor + Dokumentation + Paket-Meta-Daten + Name des Pakets + Paket enthält keine Elemente + +
+ Sie können das Paket ohne Gefahr deinstallieren indem Sie "Paket deinstallieren" anklicken.]]> +
+ Paket-Optionen + Informationen zum Paket + Paket-Repository + Deinstallation bestätigen + Paket wurde deinstalliert + Das Paket wurde erfolgreich deinstalliert + Paket deinstallieren + + + Achtung: alle Dokumente, Medien, etc, die von den zu entfernenden Elementen abhängen, + werden nicht mehr funktionieren und im Zweifelsfall kann dass gesamte CMS instabil werden. + Bitte deinstallieren Sie also mit Vorsicht. Falls Sie unsicher sind, kontaktieren Sie den Autor des Pakets.]]> + + Paketversion + Bestätigt auf Umbraco Cloud zu funktioneren + + + Einfügen mit Formatierung (Nicht empfohlen) + Der Text, den Sie einfügen möchten, enthält Sonderzeichen oder spezielle Formatierungen. Dies kann zum Beispiel beim Kopieren aus Microsoft Word heraus passieren. Umbraco kann Sonderzeichen und spezielle Formatierungen automatisch entfernen, damit der eingefügte Inhalt besser für die Veröffentlichung im Web geeignet ist. + Als reinen Text ohne jede Formatierung einfügen + Einfügen, aber Formatierung bereinigen (Empfohlen) + + + Rollenbasierter Zugriffschutz + Wenn Sie rollenbasierte Authentifikation mit Umbraco-Mitgliedsgruppen verwenden wollen. + Sie müssen zuerst eine Mitgliedsgruppe erstellen, bevor derrollenbasierte Zugriffschutz aktiviert werden kann. + Fehlerseite + Seite mit Fehlermeldung (Benutzer-Login erfolgt, aber keinen Zugriff auf die aufgerufene Seite erlaubt) + Bitte wählen Sie, auf welche Art der Zugriff auf diese Seite geschützt werden soll + %0% ist nun zugriffsgeschützt + Zugriffsschutz von %0% entfernt + Login-Seite + Seite mit Login-Formular + Zugriffsschutz entfernen + + %0% wirklich entfernen? + ]]> + + Auswahl der Seiten, die das Login-Formular und die Fehlermeldung enthalten + + Auswahl der Benutzergruppen, die Zugriff auf Seite %0% haben sollen + + %0% haben sollen.]]> + Mitglieder basierte Zugriffsberechtigung + Falls Sie Mitglieder basierte Zugriffsberechtigung gewähren wollen + + + Die Zugriffsrechte des Benutzers sind ungenügend, um alle Unterknoten zu veröffentlichen + + + + + %0% kann nicht veröffentlicht werden, da die Veröffentlichung zeitlich geplant ist. + + + + + + + + + %0% konnte nicht veröffentlicht werden, da ein Plug-In die Aktion abgebrochen hat. + + + %0% kann nicht veröffentlicht werden, da das übergeordnete Dokument nicht veröffentlicht ist. + + Unveröffentlichte Unterelemente einschließen + Bitte warten, Veröffentlichung läuft... + %0% Elemente veröffentlicht, %1% Elemente ausstehend ... + %0% wurde veröffentlicht + %0% und die untergeordneten Elemente wurden veröffentlicht + %0% und alle untergeordneten Elemente veröffentlichen + + Sichern und Veröffentlichen, um %0% zu veröffentlicht und auf der Website sichtbar zu machen.

+ Sie können dieses Element mitsamt seinen untergeordneten Elementen veröffentlichen, indem Sie Unveröffentlichte Unterelemente einschließen markieren. + ]]> +
+ + + Sie haben keine freigegeben Farben konfiguriert + + + Sie können nur Elemente folgender Typen wählen: %0% + Sie haben ein entferntes oder im Papierkorb befindliches Inhaltselement ausgewählt + Sie haben entfernte oder im Papierkorb befindliche Inhaltselemente ausgewählt + + + Element entfernen + Sie haben ein entferntes oder im Papierkorb befindliches Medienelement ausgewählt + Sie haben entfernte oder im Papierkorb befindliche medienelemente ausgewählt + Verworfen + Medien öffnen + Medientyp ändern + Ändere %0% auf %1% + Erstellen abbrechen? + + + Sie haben Änderungen an diesem Inhalt vorgenommen. Sind Sie sich sicher das Sie + diese verwerfen wollen? + + Alle Medien löschen? + Clipboard + Nicht erlaubt + Medienpicker öffnen + + + Wählen Sie einen Editor + Editor auswählen + + + Externen Link eingeben + Internen Link auswählen + Beschriftung + Link + In neuem Fenster öffnen + Bezeichnung eingeben + Link eingeben + + + Zurücksetzen + Fertig + Rückgängig machen + Benutzer definiert + + + Änderungen + Erstellt + Wählen Sie eine Version, um diese mit der aktuellen zu vergleichen + Aktuelle Version + Zeigt die Unterschiede zwischen der aktuellen und der ausgewählten Version an.<br />Text in <del>rot</del> fehlen in der ausgewählten Version, <ins>grün</ins> markierter Text wurde hinzugefügt. + Keine Unterschiede zwischen den beiden Versionen gefunden. + Dokument wurde zurückgesetzt + Zeigt die ausgewählte Version als HTML an. Wenn Sie sich die Unterschiede zwischen zwei Versionen anzeigen lassen wollen, benutzen Sie bitte die Vergleichsansicht. + Zurücksetzen auf + Version auswählen + Ansicht + + Versionen + Aktulle Bearbeitungs Version + Aktulle veröffentlichte Version + + + Skript bearbeiten + + + Inhalte + Formulare + Medien + Mitglieder + Pakete + Einstellungen + Übersetzung + Benutzer + + + Touren + Die besten Umbraco-Video-Tutorials + Besuche our.umbraco.com + Besuche umbraco.tv + Schaue gratis Tutorials + von Umbraco Learning Base + + + Standardvorlage + Wählen Sie die lokale .udt-Datei aus, die den zu importierenden Dokumenttyp enthält und fahren Sie mit dem Import fort. Die endgültige Übernahme erfolgt im Anschluss erst nach einer weiteren Bestätigung. + Beschriftung der neuen Registerkarte + Elementtyp + Typ + Stylesheet + Skript + Registerkarte + Registerkartenbeschriftung + Registerkarten + Masterdokumenttyp aktiviert + Dieser Dokumenttyp verwendet + Für dieses Register sind keine Eigenschaften definiert. Klicken Sie oben auf "neue Eigenschaft hinzufügen", um eine neue Eigenschaft hinzuzufügen. + Zugehörige Vorlage anlegen + Bildsymbol hinzufügen + + + Sortierreihenfolge + Erstellungsdatum + Sortierung abgeschlossen. + Ziehen Sie die Elemente an ihre gewünschte neue Position. + Bitte warten, die Seiten werden sortiert. Das kann einen Moment dauern. + Dieser Knoten hat keine Unterknoten zum Sortieren + + + Validierung + Validierungsfehler müssen behoben werden, bevor das Element gesichert werden kann + Fehlgeschlagen + Gesichert + Unzureichende Benutzerberechtigungen. Vorgang kann nicht abgeschlossen werden. + Abgebrochen + Vorgang wurde durch eine benutzerdefinierte Erweiterung abgebrochen + Eigenschaft existiert bereits + Eigenschaft erstellt + Name: %0% Datentyp: %1% + Eigenschaft gelöscht + Dokumenttyp gespeichert + Registerkarte erstellt + Registerkarte gelöscht + Registerkarte %0% gelöscht + Stylesheet wurde nicht gespeichert + Stylesheet gespeichert + Stylesheet erfolgreich gespeichert + Datentyp gespeichert + Wörterbucheintrag gespeichert + Inhalt veröffentlicht + und ist auf der Website sichtbar + %0% Documente veröffentlicht und auf der Website sichtbar + %0% veröffentlicht und auf der Website sichtbar + %0% Documente veröffentlicht in Sprache %1% und auf der Website sichtbar + Inhalte gespeichert + Denken Sie daran, die Inhalte zu veröffentlichen, um die Änderungen sichtbar zu machen + Der Termin für die Veröffentlichung wurde geändert + %0% gesichert + Zur Abnahme eingereicht + Die Änderungen wurden zur Abnahme eingereicht + %0% Änderungen wurden zur Abnahme eingereicht + Medium gespeichert + Medium fehlerfrei gespeichert + Mitglied gespeichert + Stylesheet-Regel gespeichert + Stylesheet gespeichert + Vorlage gespeichert + Fehler beim Speichern des Benutzers. + Benutzer gespeichert + Benutzertyp gepsichert + Benutzergruppe gepsichert + Datei wurde nicht gespeichert + Datei konnte nicht gespeichert werden. Bitte überprüfen Sie die Schreibrechte auf Dateiebene. + Datei gespeichert + Datei erfolgreich gespeichert + Sprache gespeichert + Medientyp gespeichert + Mitgliedertyp gespeichert + Mitgliedergruppe gespeichert + Vorlage wurde nicht gespeichert + Bitte prüfen Sie, ob möglicherweise zwei Vorlagen den gleichen Alias verwenden. + Vorlage gespeichert + Vorlage erfolgreich gespeichert! + Veröffentlichung des Inhalts aufgehoben + Inhaltsvariante %0% unveröffentlicht + Die Veröffentlichung der Pflichtsprache '%0%' wurde zurück genommen. Das gleiche gilt für alle Sprachen dieses Inhalts. + Partielle Ansicht gespeichert + Partielle Ansicht ohne Fehler gespeichert. + Partielle Ansicht nicht gespeichert + Fehler beim Speichern der Datei. + Berechtigungen gesichert für + %0% Benutzergruppen entfernt + %0% wurde entfernt + %0% Benutzer aktiviert + %0% Benutzer deaktiviert + %0% ist jetzt aktiviert + %0% ist jetzt deaktiviert + Benutzergruppen wurden gesetzt + %0% Benutzer freigegeben + %0% ist jetzt freigegeben + Mitglied wurde in Datei exportiert + Beim Exportieren des Mitglieds trat ein Fehler auf + Benutzer %0% wurde entfernt + Benutzer einladen + Einladung wurde erneut an %0% geschickt + Das Dokument kann nicht veröffentlicht werden, solange '%0%' nicht veröffentlicht wurde + Validierung fehlgeschlagen für Sprache '%0%' + Dokumenttyp wurde in eine Datei exportiert + Beim Exportieren des Dokumenttyps trat ein Fehler auf + Das Veröffentlichungsdatum kann nicht in der Vergangenheit liegen + Die Veröffentlichung kann nicht eingeplant werden, solange '%0%' (benötigt) nicht veröffentlicht wurde + Die Veröffentlichung kann nicht eingeplant werden, solange '%0%' (benötigt) ein späteres Veröffentlichungsdatum hat als eine optionale Sprache + Das Ablaufdatum darf nicht in der Vergangenheit liegen + Das Ablaufdatum darf nicht vor dem Veröffentlichungsdatum liegen + + + Neuer Stil + Stil bearbeiten + Rich text editor Stile + Definiere die Styles, die im Rich-Text-Editor dieses Stylesheets verfügbar sein sollen. + Stylesheet bearbeiten + Stylesheet-Regel bearbeiten + Bezeichnung im Auswahlmenü des Rich-Text-Editors + Vorschau + So wird der Text im Rich-Text-Editor aussehen. + Selector + Benutze CSS Syntax, z. B.: "h1" oder ".redHeader" + Stile + Die CSS-Auszeichnungen, die im Rich-Text-Editor verwendet werden soll, z. B.: "color:red;" + Kode + Rich Text Editor + + + Beim Entfernen der Vorlage mit Id %0% trat ein Fehler auf + Vorlage bearbeiten + Bereich + Platzhalter-Bereich verwenden + Platzhalter einfügen + Einfügen + Wählen Sie, was in die Vorlage eingefügt werden soll + Wörterbucheintrag einfügen + Ein Wörterbuchelement ist ein Platzhalter für lokalisierbaren Text. Das macht es einfach mehrsprachige Websites zu gestalten. + Makro + + Ein Makro ist eine konfigurierbare Komponente, die großartig + für wiederverwendbare Teile Ihres Entwurfes sind, + für welche Sie optionale Parameter benötigen, wie z. B. Galerien, Formulare oder Listen. + + Umbraco-Feld + + Zeigt den Wert eines benannten Feldes der aktuellen Seite an, mit der Möglichkeit den Wert zu verändern + oder einen alternativen Ersatzwert zu wählen. + + Teilansicht (Partial View) + + Eine Teilansicht ist eine eigenständige Vorlagen-Datei, die innerhalb einer anderen Vorlage verwendet werden kann. + Sie ist gut geeignet, um "Markup"-Kode wiederzuverwenden oder komplexe Vorlagen in mehrere Dateien aufzuteilen. + + Basisvorlage + Keine Basis + Untergeordnete Vorlage einfügen + + @RenderBody() Platzhalters. + ]]> + + Definiert einen benannten Bereich + + @section { ... }. + Dieser benannte Bereich kann in der übergeordneten Vorlage + durch Verwendung von @RenderSection eingefügt werden. + ]]> + + Füge einen benannten Bereich ein + + @RenderSection(name) ein. + Dies verarbeitet einen benannten Bereich einer untergeordneten Vorlage, der mit @section [name]{ ... } umschlossen, definiert wurde. + ]]> + + Bereichsname + Bereich ist notwendig + + @section Definition gleichen Namens enthalten, + anderfalls tritt ein Fehler auf. + ]]> + + Abfrage-Generator + zurückgegebene Elemente, in + Ich möchte + den ganzen Inhalt + Inhalt vom Typ "%0%" + von + meiner website + wobei + und + ist + ist nicht + vor + vor (inkl. gewähltes Datum) + nach + nach (inkl. gewähltes Datum) + gleich + ungleich + enthält + ohne + größer als + größer als oder gleich + weniger als + weniger als oder gleich + Id + Name + Datum der Erzeugung + Datum der letzten Aktualisierung + sortiert nach + aufsteigend + absteigend + Vorlage + + + Image + Macro + Neues Element + Layout auswählen + Neue Zeile + Neuer Inhalt + Inhalt entfernen + Einstellungen anwenden + nicht zugelassen]]> + Dieser Inhalt ist hier zugelassen + Klicken, um Inhalt einzubetten + Klicken, um Abbildung einzufügen + Hier schreiben ... + Layouts + Layouts sind die grundlegenden Arbeitsflächen für das Gestaltungsraster. Üblicherweise sind nicht mehr als ein oder zwei Layouts nötig. + Layout hinzufügen + Passen Sie das Layout an, indem Sie die Spaltenbreiten einstellen und Abschnitte hinzufügen. + Einstellungen für das Zeilenlayout + Zeilen sind vordefinierte horizontale Zellenanordnungen + Zeilenlayout hinzufügen + Passen Sie das Zeilenlayout an, indem Sie die Zellenbreite einstellen und Zellen hinzufügen. + Spalten + Insgesamte Spaltenanzahl im Layout + Einstellungen + Legen Sie fest, welche Einstellungen die Autoren anpassen können. + CSS-Stile + Legen Sie fest, welche Stile die Autoren anpassen können. + Alle Elemente erlauben + Alle Zeilenlayouts erlauben + Maximal erlaubte Elemente + Leer lassen oder auf 0 setzen für unbegrenzt + Als Standard setzen + Extra wählen + Standard wählen + wurde hinzugefügt + + + Mischungen + Gruppe + Sie haben keine Gruppen hinzugefügt + Gruppe hinzufügen + Übernimm von + Eigenschaft hinzufügen + Notwendige Bezeichnung + Listenansicht aktivieren + + Konfiguriert die Verwendung einer sortier- und filterbaren Listenansicht der Unterknoten für diesen Dokumenttyp. + Die Unterknoten werden nicht in Baumstruktur angezeigt. + + Erlaubte Vorlagen + + Wählen Sie die Vorlagen, die Editoren für diesen Dokumenttyp wählen dürfen + + Als Wurzelknoten zulassen + + Ermöglicht es Editoren diesen Dokumenttyp in der obersten Ebene der Inhalt-Baum-Strukur zu wählen + + Erlaubte Dokumenttypen für Unterknoten + + Erlaubt es Inhalt der angegebenen Typen unterhalb Inhalten dieses Typs anzulegen + + Wählen Sie einen Unterknoten + Übernimm Tabs und Eigenschaften vone einem vorhandenen Inhaltstyp. Neue Tabs werden zum vorliegenden Inhaltstyp hinzugefügt oder mit einem gleichnamigen Tab zusammengeführt. + Dieser Inhaltstyp wird in einer Mischung verwendet und kann deshalb nicht selbst zusammengemischt werden. + Es sind keine Inhaltstypen für eine Mischung vorhanden. + Neu anlegen + Vorhandenen nutzen + Editor-Einstellungen + Konfiguration + Ja, entferne + wurde verschoben unter + wurde kopiert unter + Wähle den Ordner in den verschoben wird + Wähle den Ordner in den kopiert wird + in der untenstehenden Baumstruktur + Alle Dokumenttypen + Alle Inhalte + Alle Medien + welche auf diesem Dokumenttyp beruhen, werden unwiderruflich entfernt, bitte bestätigen Sie, dass diese ebenfalls entfernt werden sollen. + welche auf diesem Medientyp beruhen, werden unwiderruflich entfernt, bitte bestätigen Sie, dass diese ebenfalls entfernt werden sollen. + welche auf diesem Mitgliedstyp beruhen, werden unwiderruflich entfernt, bitte bestätigen Sie, dass diese ebenfalls entfernt werden sollen. + und alle Inhalte, die auf diesem Typ basieren + und alle Medienelemente, die auf diesem Typ basieren + und alle Mitglieder, die auf diesem Typ basieren + Mitglied kann bearbeiten + + Diese Eigenschaft zur Bearbeitung des Mitglieds auf seiner Profileseite freigeben + + sensibelle Daten + + Diese Eigenschaft für Editoren, die keine Berechtigung für sensibelle Daten haben, verbergen + + Auf Mitgliedsprofil anzeigen + Diesen Eigenschaftswert für die Anzeige auf der Profilseite des Mitglieds zulassen + Tab hat keine Sortierung + Wo wird diese Mischung verwendet? + + Diese Mischung wird aktuell in den Mischungen folgender Dokumenttypen verwendet: + + Kultur basierte Variationen zulassen + Editoren erlauben, Inhalt dieses Typs in verschiedenen Sprachen anzulegen + Kultur basierte Variationen zulassen + Ist ein Elementtyp + + Nested Content vorgesehen, nicht jedoch als Inhalt-Knoten in der Baumstruktur + ]]> + + Dies kann nicht für Elementtypen verwendet werden + + + Sparche hinzufügen + Notwendige Sprache + Eigenschaften müssen für diese Sprache ausgefüllt sein bevor ein Knoten veröffentlicht werden kann. + Standardsprache + Eine Umbraco site kann nur eine Standardsprache haben. + Ändern der Standardsprache kann zum Fehlen von Standard-Inhalt führen. + Wird ersetzt durch + Kein Ersatzsprache + + Um mehrsprachigem Inhalt zu ermöglichen durch eine andere Sprache ersetzt zu werden, + falls die angefragte Sprache nicht verfügbar ist, wählen Sie diese Option hier aus. + + Ersatzsprache + Keine + %0% wird zwischen Sprachen und Segmenten geteilt.]]> + %0% wird zwischen allen Sprachen geteilt.]]> + %0% wird zwischen allen Segmenten geteilt.]]> + Geteilt: Sprachen + Geteilt: Segmente + + + Parameter hinzufügen + Parameter bearbeiten + Makroname vergeben + Parameter + Definiere die Parameter, die verfügbar sein sollen, wenn dieses Makro verwendet wird. + + + Datenmodel erzeugen + Keine Sorge, das kann eine Weile dauern + Datenmodel erzeugt + Datenmodel konnte nicht erzeugt werden + Erzeugung des Datenmodels fehlgeschlagen, siehe Ausnahmen in den Log-Daten + + + Standardwert hinzufügen + Standardwert + Alternatives Feld + Alternativer Text + Groß- und Kleinschreibung + Kodierung + Feld auswählen + Zeilenumbrüche ersetzen + Ersetzt Zeilenumbrüche durch das HTML-Tag <br /> + Benutzerdefinierte Felder + nur Datum + Als Datum formatieren + HTML kodieren + Wandelt Sonderzeichen in HTML-Zeichencodes um + Wird nach dem Feldinhalt eingefügt + Wird vor dem Feldinhalt eingefügt + Kleinbuchstaben + Keine + Beispiel-Ausgabe + An den Feldinhalt anhängen + Dem Feldinhalt voranstellen + Rekursiv + Ja, verwende es rekursiv + Standardfelder + Großbuchstaben + URL kodieren + Wandelt Sonderzeichen zur Verwendung in URLs um + Wird nur verwendet, wenn beide vorgenannten Felder leer sind + Dieses Feld wird nur verwendet, wenn das primäre Feld leer ist + Datum und Zeit + + + Details zur Übersetzung + Herunterladen der XML-Defintionen (XML-DTD) + Felder + Einschließlich der Unterseiten + + + + Bitte erstellen Sie zuerst mindestens einen Übersetzer. + Die Seite '%0%' wurde zur Übersetzung gesendet + Sendet die Seite '%0%' zur Übersetzung + Anzahl der Wörter + Übersetzen in + Übersetzung abgeschlossen. + Sie können eine Vorschau der Seiten anzeigen, die Sie gerade übersetzt haben, indem Sie sie unten anklicken. Wenn die Originalseite zugeordnet werden kann, erhalten Sie einen Vergleich der beiden Seiten angezeigt. + Übersetzung fehlgeschlagen, die XML-Datei könnte beschädigt oder falsch formatiert sein + Übersetzungsoptionen + Übersetzer + Hochladen der XML-Übersetzungsdatei + + + Inhalt + Inhalt-Vorlage + Medien + Zwischenspeicher + Papierkorb + Erstellte Pakete + Datentypen + Wörterbuch + Installierte Pakete + Design-Skin installieren + Starter-Kit installieren + Sprachen + Lokales Paket hochladen und installieren + Makros + Medientypen + Mitglieder + Mitgliedergruppen + Mitgliederrollen + Mitglieder-Typen + Dokumententypen + Relationstypen + Pakete + Pakete + Teilansicht (Partial View) + Makro-Teilansicht(Partial View Macro Files) + Paket-Repositories + 'Runway' installieren + Runway-Module + Server-Skripte + Client-Skripte + Stylesheets + Vorlagen + Log-Einträge anzeigen + Benutzer + Einstellungen + Vorlagen + Drittanbieter + + + Neues Update verfügbar + %0% verfügbar, hier klicken zum Herunterladen + Keine Verbindung zum Update-Server + Fehler beim Überprüfen der Updates. Weitere Informationen finden Sie im Stacktrace. + + + Zugang + Basierend auf den zugewiesenen Gruppen und Startknoten, hat der Benutzer Zugang zu folgenden Knoten + Zugang zuweisen + Administrator + Feld für Kategorie + Benutzer angelegt + Kennwort ändern + Foto ändern + Benötigt - geben Sie eine Email Adresse für diesen User an + Neues Kennwort + Noch %0% Zeichen benötigt! + Es sollten mindestens %0% Sonderzeichen verwendet werden. + wurde nicht ausgeschlossen + Das Kennwort wurde nicht geändert + Neues Kennwort (Bestätigung) + Sie können Ihr Kennwort für den Zugriff auf den Umbraco-Verwaltungsbereich ändern, indem Sie das nachfolgende Formular ausfüllen und auf 'Kennwort ändern' klicken + Schnittstelle für externe Editoren + Weiteren Benutzer anlegen + + Lege neue Benutzer an, um ihnen Zugang zum Umbraco-Back-Office zu geben. + Während des Anlegens eines neuen Benutzer wird ein Kennwort erzeugt, das Sie dem Benutzer mitteilen können. + + Feld für Beschreibung + Benutzer endgültig deaktivieren + Dokumenttyp + Editor + Feld für Textausschnitt + Fehlgeschlagene Anmeldeversuche + Benutzerprofil aufrufen + Gruppen hinzufügen, um Zugang und Berechtigungen zuzuweisen + Weitere Benutzer einladen + + Laden Sie neue Benutzer ein, um ihnen Zugang zum Umbraco-Back-Office zu geben. + Eine Einladungs-E-Mail wird an dem Benutzer geschickt. Diese enthält Informationen, wie sich der Benutzer im Umbraco-Back-Office anmelden kann. + Einladungen sind 72 Stunden lang gültig. + + Sprache + Bestimmen Sie die Sprache für Menüs und Dialoge + Letztes Abmeldedatum + Letzte Anmeldung + letzte Änderung des Kennworts + Benutzername + Startelement in der Medienbibliothek + Beschränke die Medien-Bibliothek auf einen bestimmen Startknoten + Medien-Startknoten + Beschränke die Medien-Bibliothek auf bestimme Startknoten + Bereiche + Umbraco-Back-Office sperren + hat sich noch nie angemeldet + Altes Kennwort + Kennwort + Kennwort zurücksetzen + Ihr Kennwort wurde geändert! + Bitte bestätigen Sie das neue Kennwort + Geben Sie Ihr neues Kennwort ein + Ihr neues Kennwort darf nicht leer sein! + Aktuelles Kennwort + Aktuelles Kennwort falsch + Ihr neues Kennwort und die Wiederholung Ihres neuen Kennworts stimmen nicht überein. Bitte versuchen Sie es erneut! + Die Bestätigung Ihres Kennworts stimmt nicht mit dem angegebenen neuen Kennwort überein! + Die Berechtigungen der untergeordneten Elemente ersetzen + Die Berechtigungen für folgende Seiten werden angepasst: + Dokumente auswählen, um deren Berechtigungen zu ändern + Foto entfernen + Normale Berechtigungen + Detailierte Berechtigungen + Knoten basierte Berechtigungen vergeben + Profil + Untergeordnete Elemente durchsuchen + Bereiche hinzufügen, um Benutzern Zugang zu gewähren + Wählen Sie Benutzergruppen + Kein Startknoten ausgewählt + Keine Startknoten ausgewählt + Startknoten in den Inhalten + Inhalt auf bestimmt Startknoten beschränken + Startknoten in den Inhalten + Inhalt auf bestimmte Startknoten beschränken + Benutzer zuletzt aktualiert + wurde angelegt + Der Benutzer wurde erfolgreich angelegt. Zu Anmelden im Umbraco-Back-Office verwenden Sie bitte folgendes Kennwort: + Benutzer Verwaltung + Benutzername + Berechtigungen + Benutzergruppe + wurde eingeladen + Eine Einladung mit Anweisungen zur Anmeldung im Umbraco-Back-Office wurde dem neuen Benutzer zugeschickt. + Hallo und Willkommen bei Umbraco! In nur einer Minute sind Sie bereit loszulegen, Sie müssen nur ein Kennwort festlegen. + Willkommen bei Umbraco! Bedauerlicherweise ist Ihre Einladung verfallen. Bitte kontaktieren Sie Ihren Administrator und bitten Sie ihn, diese erneut zu schicken. + Autor + Änderung + + Ihr Profil + Ihr Verlauf + Sitzung läuft ab in + Benutzer einladen + Benutzer anlegen + Einladung schicken + Zurück zu den Benutzern + Umbraco: Einladung + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hallo %0%, +

+

+ Sie wurden von %1% ins Umbraco-Back-Office eingeladen. +

+

+ Nachricht von %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Um diese Einladung anzunehmen,
klicken Sie diese Schaltfläche +
+
+
+

+ Falls die Schaltfläche nicht funktioniert, kopieren Sie folgenden URL und fügen ihn im Browser ein:

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + +]]> +
+ Einladung erneut verschicken... + Benutzer entfernen + Wollen Sie dieses Benutzerkonto wirklich entfernen? + Alle + Aktiv + Gesperrt + Ausgeschlossen + Eingeladen + Nicht aktiv + Name (A-Z) + Name (Z-A) + Oldest + Newest + Last login + + + Validierung + Prüfe auf gültiges E-Mail-Format + Prüfe auf gültiges Zahlen-Format + Prüfe auf gültiges URL-Format + ...oder verwende eigene Validierung + Pflichtfeld + Regulären Ausdruck eingeben + Fügen Sie mindestens + Fügen Sie maximal + Element(e) hinzu + Element(e) ausgewählt + Ungültiges Datum + Keine Zahl + Ungültiges E-Mail-Format + Der Wert darf nicht ungesetzt bleiben + Der Wert darf nicht leer bleiben + Der Wert ist ungültig + Eigene Validierung + + + + Wert wurde auf den empfohlenen Wert gesetzt: '%0%'. + Erwartete Wert '%1%' für '%2%' in der Konfigurationsdatei '%3%', '%0%' wurde jedoch gefunden. + Unerwarteten Wert '%0%' für '%2%' in der Konfigurationsdatei '%3%' gefunden. + "MacroErrors" auf '%0%' gesetzt. + + "MacroErrors" sind auf '%0%' gesetzt, + was verhindert, dass einige oder alle Seiten Ihrer Website vollständig geladen werden, falls Fehler in Makros auftreten. Schaltfläche "Beheben" setzt den Wert auf '%1%'. + + Ihr Website-Zertifikat (SSL) ist gültig. + (SSL-)Zertifikat-Validierungsfehler: '%0%' + Ihr Website-Zertifikat (SSL) ist abgelaufen. + Ihr Website-Zertifikat (SSL) wird in %0% Tagen ablaufen. + Fehler beim PINGen der URL %0% - '%1%' + Sie betrachten diese Website %0% unter Verwendung des HTTPS-Schemas. + 'Debug' Kompilierungsmodus ist abgeschaltet. + 'Debug' Kompilierungsmodus ist gegenwertig eingeschaltet. Es ist empfehlenswert diesen vor Live-Gang abzuschalten. + + X-Frame-Options ist vorhanden. Diese dienen zur Kontrolle, ob eine Site in IFRAMES anderer Sites angezeigt werden kann.]]> + X-Frame-Options ist nicht vorhanden. Es dient zur Kontrolle, ob eine Site in IFRAMES anderer Sites angezeigt werden kann.]]> + X-Content-Type-Options ist vorhanden. Diese dienen zum Schutz gegen MIME-'Schnüffeln'-Schwachstellen. ]]> + X-Content-Type-Options ist nicht vorhanden. Diese dienen zum Schutz gegen MIME-'Schnüffeln'-Schwachstellen. ]]> + Strict-Transport-Security, auch bekannt als HSTS-Header, ist vorhanden.]]> + Strict-Transport-Security, auch bekannt als HSTS-Header, ist nicht vorhanden.]]> + X-XSS-Protection ist vorhanden.]]> + X-XSS-Protection ist nicht vorhanden]]> + %0%.]]> + Es sind keine Header, die Informationen über die Website-Technologie preisgeben, vorhanden. + Die SMTP-Einstellungen sind korrekt konfiguriert und der Dienst arbeitet wie erwartet. + %0% eingestellt.]]> + %0% gestellt.]]> + +

+ Die Ergebnisse der geplanten Systemzustandsprüfung läuft am %0% um %1% lauten wie folgt: +

%2% + ]]> +
+ Status der Umbraco Systemzustand: %0% + + + URL-Änderungsaufzeichnung abschalten + URL-Änderungsaufzeichnung einschalten + Kultur + Original URL + Weiterleiten zu + URL-Weiterleitungen verwalten + Die folgenden URLs leiten auf diesen Inhalt: + Es wurden keine Weiterleitungen angelegt + + Wenn eine veröffentlichte Seite umbenannt oder verschoben wird, + erzeugt dieses CMS automatisch eine entsprechende Weiterleitung. + + URL-Weiterleitung wurde entfernt. + Beim Entfernen der URL-Weiterleitung ist ein Fehler aufgetreten. + Dies entfernt die Weiterleitung + Wollen Sie die URL-Änderungsaufzeichnung wirklich abschalten? + Die URL-Änderungsaufzeichnung wurde abgeschaltet. + Fehler während der Abschaltung der URL-Änderungsaufzeichnung, weitere Information finden Sie in den Log-Dateien. + Die URL-Änderungsaufzeichnung wurde eingeschaltet. + Fehler während der Aktivierung der URL-Änderungsaufzeichnung, weitere Information finden Sie in den Log-Dateien. + + + Das Wörterbuch ist leer + + + Buchstaben verbleiben + + + Inhalt mit Id = {0} des Oberknotens mit Id = {1} wurde verworfen + Medienelement mit Id = {0} des Oberknotens mit Id = {1} wurde verworfen + Dieses Element kann nicht automatisch wiederhergestellt werden + + Es gibt keine Position für das automatische Wiederherstellen dieses Elementes. + Sie können es manuell mit Hilfe der untenstehenden Baumstruktur verschieben. + + wurde wiederhergestellt unterhalb von + + + Richtung + Ober- zu Unterknoten + Bidirektional + Oberknoten + Unterknoten + Anzahl + + Relationen + Angelegt + Kommentar + Name + Es gibt keine Relationen für diesen Typ. + Relationentyp + Relationen + + + Lassen Sie uns beginnen + URL-Weiterleitungen verwalten + Inhalt + Begrüßung + Examine Management + Status der Veröffentlichungen + Models Builder + Systemzustand prüfen + Lassen Sie uns beginnen + Umbraco Forms installieren + + + zurück gehen + Aktives Layout: + Springe zu + Gruppe + bestanden + alarmierend + fehlgeschlagen + Vorschlag + Prüfung bestanden + Prüfung fehlgeschlagen + Back-Office Suche öffnen + Back-Office Hilfe öffnen / schliessen + Ihre Profil-Einstellungen öffnen / schliessen + + + Wählen Sie Alle + Alle abwählen + + + Umbraco Forms + + Erstellen Sie Formulare mithilfe einer intuitiven Benutzeroberfläche. Von einfachen Kontaktformularen + die Email verschicken bis hin zu komplexen Fragebögen die mit einem CRM System verbunden sind. Ihre Kunden werden es lieben! + + + + Was sind Inhaltsvorlagen? + + Inhaltsvorlagen sind vordefinierte Inhalte die ausgewählt werden können + wenn Sie einen neuen Inhaltsknoten anlegen wollen. + + Wie erstelle ich eine Inhaltsvorlage? + + Es gibt zwei Möglichkeiten eine Inhaltsvorlage zu erstellen:

+
    +
  • Rechtsklicken Sie einen Inhaltsknoten und wählen Sie "Inhaltsvorlage erstellen" aus.
  • +
  • Rechtsklicken Sie den Inhaltsvorlagen-Baum und wählen Sie den Dokumententypen aus für den Sie eine Vorlage erstellen wollen.
  • +
+

Wenn Sie einen Namen vergen haben können Reakteure diese als Vorlage für neue Seiten benutzen.

+ ]]> +
+ Wie verwalte ich eine Inhaltsvorlage? + + Sie können Inhaltsvorlagen bearbeiten und löschen in dem Sie im Inhaltsvorlage-Baum die gewünschte + Vorlage auswählen. Außerdem können Sie auch direkt den Dokumenttypen bearbeiten oder löschen auf dem die Vorlage basiert + + + + Beenden + Vorschau beenden + Website Vorschau + Website in Vorschaumodus öffnen + Websitevorschau anzeigen? + + Sie haben den Vorschaumodus beendet, wollen Sie ihn erneut öffnen um + gespeicherte Version der Website anzusehen? + + Vorschau der letzten Version anzeigen + Veröffentlichte Version anzeigen + Veröffentlichte Version anzeigen? + + Sie befinden sich im Vorschaumodus, wollen Sie ihn verlassen um die letzte + veröffentlichte Version ihrer Website zu sehen? + + Veröffentlichte Version anzeigen + Im Vorschaumodus bleiben + + + Ornder erstellen + Dateien durch Packages erstellen lassen + Dateien schreiben + Medien Ordner stellen + + + Element zurückgegeben + Elemente zurückgegeben + +
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/fr_ch.xml b/src/Umbraco.Core/EmbeddedResources/Lang/fr_ch.xml new file mode 100644 index 0000000000..2690387cf3 --- /dev/null +++ b/src/Umbraco.Core/EmbeddedResources/Lang/fr_ch.xml @@ -0,0 +1,2357 @@ + + + + The Umbraco community + https://docs.umbraco.com/umbraco-cms/extending/language-files + + + Culture et noms d'hôte + Informations d'audit + Parcourir + Changer le type de document + Copier + Créer + Exporter + Créer un package + Créer un groupe + Supprimer + Désactiver + Vider la corbeille + Activer + Exporter le type de document + Importer un type de document + Importer un package + Editer dans Canvas + Déconnexion + Déplacer + Notifications + Accès public + Publier + Dépublier + Rafraîchir + Republier le site tout entier + Renommer + Récupérer + Choisissez où copier + Choisissez où déplacer + Choisissez où importer + dans l'arborescence ci-dessous + a été déplacé vers + a été copié vers + a été supprimé + Permissions + Version antérieure + Envoyer pour publication + Envoyer pour traduction + Spécifier le groupe + Trier + Traduire + Mettre à jour + Spécifier les permissions + Débloquer + Créer un modèle de contenu + Envoyer à nouveau l'invitation + + + Contenu + Administration + Structure + Autre + + + Permettre d'attribuer la culture et des noms d'hôte + Permettre d'accéder au journal d'historique d'un noeud + Permettre d'accéder à un noeud + Permettre de modifier le type de document d'un noeud + Permettre de copier un noeud + Permettre de créer des noeuds + Permettre de supprimer des noeuds + Permettre de déplacer un noeud + Permettre de définir et modifier l'accès public à un noeud + Permettre de publier un noeud + Permettre d'annuler la publication d'un noeud + Permettre de modifier les permissions pour un noeud + Permettre de revenir à une situation antérieure + Permettre d'envoyer un noeud pour approbation avant publication + Permettre d'envoyer un noeud à la traduction + Permettre de modifier l'ordonnancement des noeuds + Permettre de traduire un noeud + Permettre de sauvegarder un noeud + Permettre la création d'un Modèle de Contenu + + + Contenu + Info + + + Permission refusée. + Ajouter un nouveau domaine + Supprimer + Noeud invalide. + Domaine invalide. + Domaine déjà assigné. + Langue + Domaine + Nouveau domaine '%0%' créé + Domaine '%0%' supprimé + Domaine '%0%' déjà assigné + Domaine '%0%' mis à jour + Editer les domaines actuels + + + + Hériter + Culture + + ou hériter de la culture des noeuds parents. S'appliquera aussi
+ au noeud courant, à moins qu'un domaine ci-dessous soit aussi d'application.]]> +
+ Domaines + + + Vider la sélection + Choisir + Faire autre chose + Gras + Annuler l'indentation de paragraphe + Insérer un champ de formulaire + Insérer un entête graphique + Editer le HTML + Indenter le paragraphe + Italique + Centrer + Justifier à gauche + Justifier à droite + Insérer un lien + Insérer un lien local (ancre) + Liste à puces + Liste numérique + Insérer une macro + Insérer une image + Publier et fermer + Publier avec les descendants + Editer les relations + Retourner à la liste + Sauver + Sauver et fermer + Sauver et publier + Sauver et envoyer pour approbation + Sauver la mise en page de la liste + Planifier + Prévisualiser + Prévisualiser + La prévisualisation est désactivée car aucun modèle n'a été assigné. + Choisir un style + Afficher les styles + Insérer un tableau + Générer les modèles et fermer + Sauver et générer les modèles + Défaire + Refaire + Supprimer un tag + Annuler + Confirmer + Options de publication supplémentaires + + + Media supprimé + Media déplacé + Media copié + Media sauvegardé + + + Aperçu pour + Contenu supprimé + Contenu dé-publié + Contenu dé-publié pour les langues : %0% + Contenu publié + Contenu publié pour les langues : %0% + Contenu sauvegardé + Contenu sauvegardé pour les langues : %0% + Contenu déplacé + Contenu copié + Contenu restauré + Contenu envoyé pour publication + Contenu envoyé pour publication pour les langues : %0% + Ordonnancement des sous-éléments réalisé par l'utilisateur + Copier + Publier + Publier + Déplacer + Sauvegarder + Sauvegarder + Supprimer + Annuler publication + Annuler publication + Restaurer + Envoyer pour publication + Envoyer pour publication + Ordonner + Historique (toutes variantes) + + + Le nom du dossier ne peut pas contenir de caractères illégaux. + Echec de la suppression de l'élément : %0% + + + A été publié + A propos de cette page + Alias + (comment décririez-vous l'image oralement) + Liens alternatifs + Cliquez pour éditer cet élément + Créé par + Auteur original + Mis à jour par + Créé + Date/heure à laquelle ce document a été créé + Type de Document + Edition + Expire le + Cet élément a été modifié après la publication + Cet élément n'est pas publié + Dernière publication + Il n'y a aucun élément à afficher + Il n'y a aucun élément à afficher dans cette liste. + Aucun contenu n'a encore été ajouté + Aucun membre n'a encore été ajouté + Type de Média + Lien vers des média(s) + Groupe de membres + Rôle + Type de membre + Aucune modification n'a été faite + Aucune date choisie + Titre de la page + Ce média n'a pas de lien + Aucun contenu ne peut être ajouté pour cet élément + Propriétés + Ce document est publié mais n'est pas visible car son parent '%0%' n'est pas publié + Cette culture est publiée mais n'est pas visible car elle n'est pas publiée pour le parent '%0%' + Ce document est publié mais n'est pas présent dans le cache + Oups: impossible d'obtenir cet URL (erreur interne - voir fichier log) + Ce document est publié mais son URL entrerait en collision avec le contenu %0% + Ce document est publié mais son URL ne peut pas être routé + Publier + Publié + Publié (changements en cours) + Statut de publication + Publier avec ses descendants pour publier %0% et tous les éléments de contenu en-dessous, rendant de ce fait leur contenu accessible publiquement.]]> + Publier avec ses descendants pour publier les langues sélectionnées et les mêmes langues des éléments de contenu en-dessous, rendant de ce fait leur contenu accessible publiquement.]]> + Publié le + Dépublié le + Supprimer la date + Défininir la date + Ordre de tri mis à jour + Pour trier les noeuds, faites-les simplement glisser à l'aide de la souris ou cliquez sur les entêtes de colonne. Vous pouvez séléctionner plusieurs noeuds en gardant la touche "shift" ou "ctrl" enfoncée pendant votre séléction. + Statistiques + Titre (optionnel) + Texte alternatif (optionnel) + Légende (optionnel) + Type + Dépublier + Dépublié + Non créé + Dernière édition + Date/heure à laquelle ce document a été édité + Supprimer le(s) fichier(s) + Lien vers un document + Membre du/des groupe(s) + Pas membre du/des groupe(s) + Eléments enfants + Cible + Ceci se traduit par l'heure suivante sur le serveur : + Qu'est-ce que cela signifie?]]> + Etes-vous certain(e) de vouloir supprimer cet élément? + Etes-vous certain(e) de vouloir supprimer tous les éléments? + La propriété %0% utilise l'éditeur %1% qui n'est pas supporté par Nested Content. + Aucun type de contenu n'est configuré pour cette propriété. + Ajouter un type d'élément + Sélectionner un type d'élément + Le type d'élément sélectionné ne contient aucun groupe supporté (les onglets/tabs ne sont pas supportés par cet éditeur, changez-les en groupes ou utilisez l'éditeur Block List). + Ajouter un autre champ texte + Enlever ce champ texte + Racine du contenu + Inclure les brouillons : publier également les éléments de contenu non publiés. + Cette valeur est masquée. Si vous avez besoin de pouvoir accéder à cette valeur, veuillez prendre contact avec l'administrateur du site web. + Cette valeur est masquée. + Quelles langues souhaitez-vous publier? + Quells langues souhaitez-vous envoyer pour approbation? + Quelles langues souhaitez-vous planifier? + Sélectionnez les langues à dépublier. La dépublication d'une langue obligatoire provoquera la dépublication de toutes les langues. + Prêt.e à publier? + Prêt.e à sauvegarder? + Envoyer pour approbation + Sélectionnez la date et l'heure de publication/dépublication de l'élément de contenu. + Créer nouveau + Copier du clipboard + + + %0%]]> + Vide + Sélectionner un Modèle de Contenu + Modèle de Contenu créé + Un modèle de Contenu a été créé à partir de '%0%' + Un autre Modèle de Contenu existe déjà avec le même nom + Un Modèle de Contenu est du contenu pré-défini qu'un éditeur peut sélectionner et utiliser comme base pour la création de nouveau contenu + + + Cliquez pour télécharger + ou cliquez ici pour choisir un fichier + Ce fichier ne peut pas ête chargé, il n'est pas d'un type de fichier autorisé. + Ce fichier ne peut pas être chargé, le nom du fichier n'est pas valide + La taille maximum de fichier est + Racine du média + Les dossiers parent et destination ne peuvent pas être identiques + Echec de la création d'un dossier sous le parent avec l'id %0% + Echec du changement de nom du dossier avec l'id %0% + Glissez et déposez vos fichiers dans la zone + + + Créer un nouveau membre + Tous les membres + Les groupes de membres n'ont pas de propriétés supplémentaires modifiables. + + + Echec de la copie du type de contenu + Echec du déplacement du type de contenu + + + Echec de la copie du type de media + Echec du déplacement du type de media + + + Echec de la copie du type de membre + + + Où voulez-vous créer le nouveau %0% + Créer un élément sous + Sélectionnez le type de document pour lequel vous souhaitez créer un modèle de contenu + Introduisez un nom de dossier + Choisissez un type et un titre + Types de documents sous la section Paramètres, en modifiant les Types de noeuds enfants autorisés sous les Permissions.]]> + Types de documents sous la section Paramètres.]]> + La page sélectionnée dans l'arborescence de contenu n'autorise pas la création de pages sous elle. + Modifier les permissions pour ce type de document + Créer un nouveau type de document + Types de documents sous la section Paramètres, en modifiant l'option Autoriser comme racine sous les Permissions.]]> + Types de médias dans la section Paramètres, en modifiant les Types de noeuds enfants autorisés sous les Permissions.]]> + Le media sélectionné dans l'arborescence n'autorise pas la création d'un autre media sous lui. + Modifier les permissions pour ce type de media + Type de document sans modèle + Nouveau répertoire + Nouveau type de données + Nouveau fichier javascript + Nouvelle vue partielle vide + Nouvelle macro pour vue partielle + Nouvelle vue partielle à partir d'un snippet + Nouvelle macro pour vue partielle à partir d'un snippet + Nouvelle macro pour vue partielle (sans macro) + Nouveau fichier de feuille de style + Nouveau fichier de feuille de style pour l'éditeur de texte + + + Parcourir votre site + - Cacher + Si Umbraco ne s'ouvre pas, peut-être devez-vous autoriser l'ouverture des popups pour ce site. + s'est ouvert dans une nouvelle fenêtre + Redémarrer + Visiter + Bienvenue + + + Rester + Invalider les changements + Vous avez des changements en cours + Etes-vous certain(e) de vouloir quitter cette page? - vous avez des changements en cours + La publication rendra les éléments sélectionnés visibles sur le site. + La suppression de la publication supprimera du site les éléments sélectionnés et tous leurs descendants. + La suppression de la publication supprimera du site cette page ainsi que tous ses descendants. + Vous avez des modifications en cours. Modifier le Type de Document fera disparaître ces modifications. + + + Terminé + %0% élément supprimé + %0% éléments supprimés + %0% élément sur %1% supprimé + %0% éléments sur %1% supprimés + %0% élément publié + %0% éléments publiés + %0% élément sur %1% publié + %0% éléments sur %1% publiés + %0% élément dépublié + %0% éléments dépubliés + %0% élément sur %1% dépublié + %0% éléments sur %1% dépubliés + %0% élément déplacé + %0% éléments déplacés + %0% élément sur %1% déplacé + %0% éléments sur %1% déplacés + %0% élément copié + %0% éléments copiés + %0% élément sur %1% copié + %0% éléments sur %1% copiés + + + Titre du lien + Lien + Ancrage / requête + Nom + Fermer cette fenêtre + Êtes-vous certain(e) de vouloir supprimer + %0% des %1% éléments]]> + Êtes-vous certain(e) de vouloir désactiver + Êtes-vous certain(e)? + Êtes-vous certain(e)? + Couper + Editer une entrée du Dictionnaire + Modifier la langue + Modifier le media sélectionné + Insérer un lien local (ancre) + Insérer un caractère + Insérer un entête graphique + Insérer une image + Insérer un lien + Insérer une macro + Insérer un tableau + Ceci supprimera la langue + Modifier la culture d'une langue peut être une opération lourde qui aura pour conséquence la réinitialisation de la cache de contenu et des index + Dernière modification + Lien + Lien interne : + Si vous utilisez des ancres, insérez # au début du lien + Ouvrir dans une nouvelle fenêtre? + Cette macro ne contient aucune propriété éditable + Coller + Editer les permissions pour + Définir les permissions pour + Définir les permissions pour %0% pour le groupe d'utilisateurs %1% + Sélectionnez les groupes d'utilisateurs pour lesquels vous souhaitez définir les permissions + Les éléments dans la corbeille sont en cours de suppression. Veuillez ne pas fermer cette fenêtre avant que cette opération ne soit terminée. + La corbeille est maintenant vide + Les éléments supprimés de la corbeille seront supprimés définitivement + regexlib.com rencontre actuellement des problèmes sur lesquels nous n'avons aucun contrôle. Nous sommes sincèrement désolés pour le désagrément.]]> + Rechercher une expression régulière à ajouter pour la validation d'un champ de formulaire. Exemple: 'email, 'zip-code', 'URL'. + Supprimer la macro + Champ obligatoire + Le site a été réindéxé + Le cache du site a été mis à jour. Tous les contenus publiés sont maintenant à jour. Et tous les contenus dépubliés sont restés invisibles. + Le cache du site va être mis à jour. Tous les contenus publiés seront mis à jour. Et tous les contenus dépubliés resteront invisibles. + Nombre de colonnes + Nombre de lignes + Cliquez sur l'image pour la voir en taille réelle + Sélectionner un élément + Voir l'élément de cache + Lier à l'original + Inclure les descendants + La communauté la plus amicale + Lier à la page + Ouvre le document lié dans une nouvelle fenêtre ou un nouvel onglet + Lier à un media + Sélectionner le noeud de base du contenu + Sélectionner le media + Sélectionner le type de media + Sélectionner l'icône + Sélectionner l'élément + Sélectionner le lien + Sélectionner la macro + Sélectionner le contenu + Sélectionner le type de contenu + Sélectionner le noeud de base des media + Sélectionner le membre + Sélectionner le groupe de membres + Sélectionner le type de membre + Sélectionner le noeud + Sélectionner les sections + Sélectionner les utilisateurs + Aucune icone n'a été trouvée + Il n'y a pas de paramètres pour cette macro + Il n'y a pas de macro disponible à insérer + Fournisseurs externes d'identification + Détails de l'exception + Trace d'exécution + Exception interne + Liez votre + Enlevez votre + compte + Sélectionner un éditeur + Selectionner un snippet + Ceci supprimera le noeud et toutes ses langues. Si vous souhaitez supprimer une langue spécifique, vous devriez plutôt supprimer la publication du noeud dans cette langue-là. + + + + Pour importer un élément de dictionnaire, trouvez le fichier ".udt" sur votre ordinateur en cliquant sur le bouton "Importer" (une confirmation vous sera demandée dans l'écran suivant) + + L'élément de dictionnaire n'existe pas. + L'élément parent n'existe pas. + Il n'y a pas d'élément dans le dictionnaire. + Il n'y a pas d'élément de dictionnaire dans ce fichier. + Créer un élément de dictionnaire + + + + %0%' ci-dessous. + ]]> + + Nom de Culture + + + + Aperçu du dictionaire + + + Recherches configurées + Affiche les propriétés et les outils de chaque Recherche configurée (e.g. une recherche multi-index) + Valeurs du champ + Etat de santé + L'état de santé de l'index et s'il peut être lu + Indexeurs + Info Index + Liste les propriétés de l'index + Gérer les index d'Examine + Vous permet de voir les détails de chaque index et fournit des outils pour gérer les index + Reconstruire l'index + + + Cela pourrait prendre un certain temps en fonction de la quantité de contenu présente dans votre site.
+ Il est déconseillé de reconstruire un index pendant les périodes de trafic intense sur le site web ou quand les éditeurs sont en train d'éditer du contenu. + ]]> +
+ Recherches + Rechercher dans l'index et afficher les résultats + Outils + Outils pour gérer l'index + champs + L'index ne peut pas être lu et devra être reconstruit + Le processus dure plus de temps que prévu, vérifiez les logs Umbraco afin de voir s'il y a eu des erreurs pendant cette opératon + Cet index ne peut pas être reconstruit parce qu'on ne lui a pas assigné de + IIndexPopulator + + + Votre nom d'utilisateur + Votre mot de passe + Confirmation de votre mot de passe + Nommer %0%... + Entrez un nom... + Entrez un email... + Entrez un nom d'utilisateur... + Libellé... + Entrez une description... + Rechercher... + Filtrer... + Ajouter des tags (appuyer sur enter entre chaque tag)... + Entrez votre email + Entrez un message... + Votre nom d'utilisateur est généralement votre adresse email + #value ou ?key=value + Introduisez l'alias... + Génération de l'alias... + Créer un élément + Modifier + Nom + + + Créer une liste personnalisée + Supprimer la liste personnalisée + Il existe déjà un type de contenu, un tye de media ou un type de membre avec cet alias + + + Renommé + Entrez un nouveau nom de répertoire ici + %0% a été renommé en %1% + + + Ajouter une valeur de base + Type de donnée en base de donées + GUID du Property Editor + Property editor + Boutons + Activer les paramètres avancés pour + Activer le menu contextuel + Taille maximale par défaut des images insérées + CSS associées + Afficher le libellé + Largeur et hauteur + Sélectionnez le répertoire où déplacer + dans l'arborescence ci-dessous + a été déplacé sous + %0% supprimera les propriétés et leurs données des éléments suivants]]> + Je comprends que cette action va supprimer les propriétés et les données basées sur ce Type de Données + + + Vos données ont été sauvegardées, mais avant de pouvoir publier votre page, il y a des erreurs que vous devez d'abord corriger : + Le Membership Provider n'autorise pas le changement des mots de passe (EnablePasswordRetrieval doit être défini à true) + %0% existe déjà + Des erreurs sont survenues : + Des erreurs sont survenues : + Le mot de passe doit contenir un minimum de %0% caractères et contenir au moins %1% caractère(s) non-alphanumerique + %0% doit être un entier + Le champ %0% dans l'onglet %1% est obligatoire + %0% est un champ obligatoire + %0% dans %1% n'est pas correctement formaté + %0% n'est pas correctement formaté + + + Le serveur a retourné une erreur + Le type de fichier spécifié n'est pas autorisé par l'administrateur + NOTE ! Même si CodeMirror est activé dans la configuration, il est désactivé dans Internet Explorer car il n'est pas suffisamment stable dans ce navigateur. + Veuillez remplir l'alias et le nom de la nouvelle propriété! + Il y a un problème de droits en lecture/écriture sur un fichier ou dossier spécifique + Erreur de chargement du script d'une Partial View (fichier : %0%) + Veuillez entrer un titre + Veuillez choisir un type + Vous allez définir une taille d'image supérieure à sa taille d'origine. Êtes-vous certain(e) de vouloir continuer? + Noeud de départ supprimé, contactez votre administrateur + Veuillez sélectionner du contenu avant de changer le style + Aucun style actif disponible + Veuillez placer le curseur à la gauche des deux cellules que vous voulez fusionner + Vous ne pouvez pas scinder une cellule qui n'a pas été fusionnée. + Cette propriété n'est pas valide + + + Options + A propos + Action + Actions + Ajouter + Alias + Tout + Êtes-vous certain(e)? + Retour + Retour à l'aperçu + Bord + par + Annuler + Marge de cellule + Choisir + Fermer + Fermer la fenêtre + Fermer le panel + Commenter + Confirmer + Conserver + Conserver les proportions + Contenu + Continuer + Copier + Créer + Base de données + Date + Défaut + Supprimer + Supprimé + Suppression... + Design + Dictionnaire + Dimensions + Bas + Télécharger + Editer + Edité + Eléments + Email + Erreur + Champ + Trouver + Premier + Point focal + Général + Groupes + Groupe + Hauteur + Aide + Cacher + Historique + Icône + Id + Importer + Info + Marge intérieure + Insérer + Installer + Non valide + Justifier + Libellé + Langue + Dernier + Mise en page + Liens + En cours de chargement + Bloqué + Connexion + Déconnexion + Déconnexion + Macro + Obligatoire + Message + Déplacer + Nom + Nouveau + Suivant + Non + de + Inactif + OK + Ouvrir + Actif + ou + Trier par + Mot de passe + Chemin + Un moment s'il vous plaît... + Précédent + Propriétés + En savoir plus + Reconstruire + Email de réception des données de formulaire + Corbeille + Votre corbeille est vide + Rafraîchir + Restant + Enlever + Renommer + Renouveller + Requis + Retrouver + Réessayer + Permissions + Publication Programmée + Rechercher + Désolé, nous ne pouvons pas trouver ce que vous recherchez + Aucun élément n'a été ajouté + Serveur + Paramètres + Partagé + Montrer + Afficher la page à l'envoi + Taille + Trier + Statut + Envoyer + Type + Rechercher... + sous + Haut + Mettre à jour + Upgrader + Télécharger + URL + Utilisateur + Nom d'utilisateur + Valeur + Voir + Bienvenue... + Largeur + Oui + Dossier + Résultats de recherche + Réorganiser + J'ai fini de réorganiser + Prévisualiser + Modifier le mot de passe + vers + Liste + Sauvegarde... + actuel + Intégrer + sélectionné + Avatar de + Entête + champ système + Dernière mise à jour + + + Bleu + + + Ajouter un onglet + Ajouter une propriété + Ajouter un éditeur + Ajouter un modèle + Ajouter un noeud enfant + Ajouter un enfant + Editer le type de données + Parcourir les sections + Raccourcis + afficher les raccourcis + Activer / Désactiver la vue en liste + Activer / Désactiver l'autorisation comme racine + Commenter/Décommenter les lignes + Supprimer la ligne + Copier les lignes vers le haut + Copier les lignes vers le bas + Déplacer les lignes vers le haut + Déplacer les lignes vers le bas + Général + Editeur + Activer / Désactiver les variantes de culture + + + Couleur de fond + Gras + Couleur de texte + Police + Texte + + + Page + + + Le programme d'installation ne parvient pas à se connecter à la base de données. + Votre base de données a été détectée et est identifiée comme étant + Configuration de la base de données + + installer pour installer la base de données Umbraco %0% + ]]> + + Suivant pour poursuivre.]]> + + + Veuillez contacter votre fournisseur de services internet si nécessaire. + Si vous installez Umbraco sur un ordinateur ou un serveur local, vous aurez peut-être besoin de consulter votre administrateur système.]]> + + + + Appuyez sur le bouton Upgrader pour mettre à jour votre base de données vers Umbraco %0%

+

+ N'ayez pas d'inquiétude : aucun contenu ne sera supprimé et tout continuera à fonctionner parfaitement par après ! +

+ ]]> +
+ + Appuyez sur Suivant pour + poursuivre. ]]> + + Suivant pour poursuivre la configuration]]> + Le mot de passe par défaut doit être modifié !]]> + L'utilisateur par défaut a été désactivé ou n'a pas accès à Umbraco!

Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> + Le mot de passe par défaut a été modifié avec succès depuis l'installation!

Aucune autre action n'est requise. Cliquez sur Suivant pour poursuivre.]]> + Le mot de passe a été modifié ! + Pour bien commencer, regardez nos vidéos d'introduction + Pas encore installé. + Fichiers et dossiers concernés + Plus d'informations sur la configuration des permissions + Vous devez donner à ASP.NET les droits de modification sur les fichiers/dossiers suivants + + Vos configurations de permissions sont presque parfaites !

+ Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement profit d'Umbraco.]]> +
+ Comment résoudre + Cliquez ici pour lire la version texte + tutoriel vidéo sur la définition des permissions des répertoires pour Umbraco, ou lisez la version texte.]]> + + Vos configurations de permissions pourraient poser problème ! +

+ Vous pouvez faire fonctionner Umbraco sans problèmes, mais vous ne serez pas en mesure d'installer des packages, ce qui est hautement recommandé pour tirer pleinement profit d'Umbraco.]]> +
+ + Vos configurations de permissions ne sont pas prêtes pour Umbraco ! +

+ Pour faire fonctionner Umbraco, vous aurez besoin de mettre à jour les permissions sur les fichiers/dossiers.]]> +
+ + Vos configurations de permissions sont parfaites !

+ Vous êtes prêt(e) à faire fonctionner Umbraco et à installer des packages !]]> +
+ Résoudre un problème sur un dossier + Suivez ce lien pour plus d'informations sur les problèmes avec ASP.NET et la création de dossiers + Définir les permissions de dossier + + + + Je veux démarrer "from scratch" + + Apprenez comment) + Vous pouvez toujours choisir d'installer Runway plus tard. Pour cela, allez dans la section "Développeur" et sélectionnez "Packages". + ]]> + + Vous venez de mettre en place une plateforme Umbraco toute nette. Que voulez-vous faire ensuite ? + Runway est installé + + + Voici la liste des modules recommandés, cochez ceux que vous souhaitez installer, ou regardez la liste complète des modules + ]]> + + Recommandé uniquement pour les utilisateurs expérimentés + Je veux commencer avec un site simple + + + "Runway" est un site simple qui fournit des types de documents et des modèles de base. L'installateur peut mettre en place Runway automatiquement pour vous, + mais vous pouvez facilement l'éditer, l'enrichir, ou le supprimer par la suite. Il n'est pas nécessaire, et vous pouvez parfaitement vous en passer pour utiliser Umbraco. Cela étant dit, + Runway offre une base facile, fondée sur des bonnes pratiques, pour vous permettre de commencer plus rapidement que jamais. + Si vous choisissez d'installer Runway, vous pouvez sélectionner en option des blocs de base, appelés Runway Modules, pour enrichir les pages de votre site. +

+ + Inclus avec Runway : Home page, Getting Started page, Installing Modules page.
+ Modules optionnels : Top Navigation, Sitemap, Contact, Gallery. +
+ ]]> +
+ Qu'est-ce que Runway + Etape 1/5 : Accepter la licence + Etape 2/5 : Configuration de la base de données + Etape 3/5 : Validation des permissions de fichiers + Etape 4/5 : Sécurité Umbraco + Etape 5/5 : Umbraco est prêt + Merci d'avoir choisi Umbraco + + Parcourir votre nouveau site +Vous avez installé Runway, alors pourquoi ne pas jeter un oeil au look de votre nouveau site ?]]> + + + Aide et informations complémentaires +Obtenez de l'aide de notre communauté "award winning", parcourez la documentation ou regardez quelques vidéos gratuites sur la manière de construire un site simple, d'utiliser les packages ainsi qu'un guide rapide sur la terminologie Umbraco]]> + + Umbraco %0% est installé et prêt à être utilisé + + démarrer instantanément en cliquant sur le bouton "Lancer Umbraco" ci-dessous.
+Si vous débutez avec Umbraco, vous pouvez trouver une foule de ressources dans nos pages "Getting Started".]]> +
+ + Lancer Umbraco +Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à ajouter du contenu, à mettre à jour les modèles d'affichage et feuilles de styles ou à ajouter de nouvelles fonctionnalités]]> + + La connexion à la base de données a échoué. + Umbraco Version 3 + Umbraco Version 4 + Regarder + + Umbraco %0%, qu'il s'agisse d'une nouvelle installation ou d'une mise à jour à partir de la version 3.0 +

+ Appuyez sur "suivant" pour commencer l'assistant.]]> +
+ + + Code de la Culture + Nom de la culture + + + Vous avez été inactif et la déconnexion aura lieu automatiquement dans + Renouvellez votre session maintenant pour sauvegarder votre travail + + + Bienvenue + Bienvenue + Bienvenue + Bienvenue + Bienvenue + Bienvenue + Bienvenue + Connectez-vous ci-dessous + Identifiez-vous avec + La session a expiré + © 2001 - %0%
Umbraco.com

]]>
+ Mot de passe oublié? + Un email contenant un lien pour ré-initialiser votre mot de passe sera envoyé à l'adresse spécifiée + Un email contenant les instructions de ré-initialisation de votre mot de passe sera envoyée à l'adresse spécifiée si elle correspond à nos informations. + Montrer le mot de passe + Cacher le mot de passe + Revenir au formulaire de connexion + Veuillez fournir un nouveau mot de passe + Votre mot de passe a été mis à jour + Le lien sur lequel vous avez cliqué est non valide ou a expiré. + Umbraco: Ré-initialiser le mot de passe + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Une réinitialisation de votre mot de passe a été demandée +

+

+ Votre nom d'utilisateur pour vous connecter au backoffice Umbraco est : %0% +

+

+ + + + + + +
+ + Cliquez sur ce lien pour réinitialiser votre mot de passe + +
+

+

Si vous ne pouvez pas cliquer sur le lien, recopiez cet URL dans votre navigateur :

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]> +
+ + + Tableau de bord + Sections + Contenu + + + Choisissez la page au-dessus... + %0% a été copié dans %1% + Choisissez ci-dessous l'endroit où le document %0% doit être copié + %0% a été déplacé dans %1% + Choisissez ci-dessous l'endroit où le document %0% doit être déplacé + a été choisi comme racine de votre nouveau contenu, cliquez sur 'ok' ci-dessous. + Aucun noeud n'a encore été choisi, veuillez choisir un noeud dans la liste ci-dessus avant de cliquer sur 'ok'. + Le noeud actuel n'est pas autorisé sous le noeud choisi à cause de son type + Le noeud actuel ne peut pas être déplacé dans une de ses propres sous-pages + Le noeud actuel ne peut pas exister à la racine + L'action n'est pas autorisée car vous n'avez pas les droits suffisants sur un ou plusieurs noeuds enfants. + Relier les éléments copiés à l'original + + + Editez vos notifications pour %0% + Paramètres de notification enregistrés pour + + + + Les langues suivantes ont été modifiées : %0% + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Salut %0%, +

+

+ Ceci est un email automatique pour vous informer que la tâche '%1%' a été exécutée sur la page '%2%' par l'utilisateur '%3%' +

+ + + + + + +
+ +
+ MODIFIER
+
+

+

Résumé de la mise à jour :

+ + %6% +
+

+

+ Bonne journée !

+ Avec les salutations du Robot Umbraco +

+
+
+


+
+
+ + + ]]> +
+ + Les langues suivantes ont été modifiées :

+ %0% + ]]> +
+ La notification [%0%] à propos de %1% a été executée sur %2% + Notifications + + + Actions + Créé + Créer un package + + + et localisez le package. Les packages Umbraco ont généralement une extension ".umb" ou ".zip". + ]]> + + Ceci va supprimer le package + Inclure tous les noeuds enfant + Installé + Packages installés + Ce package n'a pas de vue de configuration + Aucun package n'a encore été créé + Vous n'avez aucun package installé + 'Packages' en haut à droite de votre écran]]> + Contenu du package + Licence + Chercher des packages + Résultats pour + Nous n'avons rien pu trouver pour + Veuillez essayer de chercher un autre package ou naviguez à travers les catégories + Populaires + Nouvelles releases + a + points de karma + Information + Propriétaire + Contributeurs + Créé + Version actuelle + version .NET + Téléchargements + Coups de coeur + Compatibilité + Ce package est compatible avec les versions suivantes de Umbraco, selon les rapports des membres de la communauté. Une compatibilité complète ne peut pas être garantie pour les versions rapportées sous 100% + Sources externes + Auteur + Documentation + Meta data du package + Nom du package + Le package ne contient aucun élément + +
+ Vous pouvez supprimer tranquillement ce package de votre installation en cliquant sur "Désinstaller le package" ci-dessous.]]> +
+ Options du package + Package readme + Repository des packages + Confirmation de désinstallation + Le package a été désinstallé + Le package a été désinstallé avec succès + Désinstaller le package + + + Remarque : tous les documents, media etc. dépendant des éléments que vous supprimez vont cesser de fonctionner, ce qui peut provoquer une instabilité du système, + désinstallez donc avec prudence. En cas de doute, contactez l'auteur du package.]]> + + Version du package + + + Coller en conservant le formatage (non recommandé) + Le texte que vous tentez de coller contient des caractères spéciaux ou du formatage. Cela peut être dû à une copie d'un texte depuis Microsoft Word. Umbraco peut supprimer automatiquement les caractères spéciaux et le formatage, de manière à ce que le texte collé convienne mieux pour le Web. + Coller en tant que texte brut sans aucun formatage + Coller, mais supprimer le formatage (recommandé) + + + Protection basée sur les groupes + Si vous souhaitez donner accès à tous les utilisateurs de groupes de membres spécifiques + Vous devez créer un groupe de membres avant de pouvoir utiliser la protection basée sur les groupes + Page d'erreur + Utilisé pour les personnes connectées, mais qui n'ont pas accès + %0%]]> + %0% est maintenant protégée]]> + %0% supprimée]]> + Page de connexion + Choisissez la page qui contient le formulaire de connexion + Supprimer la protection... + %0%?]]> + Choisissez les pages qui contiennent le formulaire de connexion et les messages d'erreur + %0%]]> + %0%]]> + Protection pour des membres spécifiques + Si vous souhaitez donner accès à des membres spécifiques + + + Permissions utilisateur insuffisantes pour publier tous les documents enfants. + + + + + + + + + + + + + + + + + + + + La validation a échoué pour la langue obligatoire '%0%'. Cette langue a été sauvegardée mais pas publiée. + Publication en cours - veuillez patienter... + %0% pages sur %1% ont été publiées... + %0% a été publié + %0% et ses pages enfants ont été publiées + Publier %0% et toutes ses pages enfant + + Publier pour publier %0% et la rendre ainsi accessible publiquement.

+ Vous pouvez publier cette page et toutes ses sous-pages en cochant Inclure les pages enfant non pubiées ci-dessous. + ]]> +
+ + + Vous n'avez configuré aucune couleur approuvée + + + Vous pouvez uniquement sélectionner des éléments du(des) type(s) : %0% + Vous avez choisi un élément de contenu actuellement supprimé ou dans la corbeille + Vous avez choisi des éléments de contenu actuellement supprimés ou dans la corbeille + + + Elément supprimé + Vous avez choisi un élément media actuellement supprimé ou dans la corbeille + Vous avez choisi des éléments media actuellement supprimés ou dans la corbeille + Mis dans la corbeille + + + introduire un lien externe + choisir une page interne + Légende + Lien + Ouvrir dans une nouvelle fenêtre + introduisez la légende à afficher + Introduiser le lien + + + Réinitialiser + Terminé + Annuler les modifications + + + Sélectionnez une version à comparer avec la version actuelle + Le texte en Rouge signifie qu'il a été supprimé de la version choisie, vert signifie ajouté]]> + Le document a été restauré à une version antérieure + Ceci affiche la version choisie en tant que HTML, si vous souhaitez voir les différences entre les deux versions en même temps, utilisez la vue différentielle + Revenir à + Choisissez une version + Voir + + Versions + Version de travail + Version publiée + + + Editer le fichier de script + + + Contenu + Formulaires + Medias + Membres + Packages + Configuration + Traduction + Utilisateurs + + + Les meilleurs tutoriels vidéo Umbraco + + + Modèle par défaut + Pour importer un type de document, trouvez le fichier ".udt" sur votre ordinateur en cliquant sur le bouton "Parcourir" et cliquez sur "Importer" (une confirmation vous sera demandée à l'écran suivant) + Titre du nouvel onglet + Type de noeud + Type + Feuille de style + Script + Onglet + Titre de l'onglet + Onglets + Type de contenu de base activé + Ce type de contenu utilise + Aucune propriété définie dans cet onglet. Cliquez sur le lien "Ajouter une nouvelle propriété" en-haut pour créer une nouvelle propriété. + Créer le template correspondant + Ajouter une icône + + + Ordre de tri + Date de création + Tri achevé. + Faites glisser les différents éléments vers le haut ou vers le bas pour définir la manière dont ils doivent être organisés. Ou cliquez sur les en-têtes de colonnes pour trier la collection complète d'éléments + + Ce noeud n'a aucun noeud enfant à trier + + + Validation + Les erreurs de validation doivent être corrigées avant de pouvoir sauvegarder l'élément + Echec + Sauvegardé + Sauvegardé. Veuillez rafraîchir votre navigateur pour voir les changements + Permissions utilisateur insuffisantes, l'opération n'a pas pu être complétée + Annulation + L'opération a été annulée par une extension tierce + Le type de propriété existe déjà + Type de propriété créé + Type de données : %1%]]> + Type de propriété supprimé + Type de document sauvegardé + Onglet créé + Onglet supprimé + Onglet avec l'ID : %0% supprimé + Feuille de style non sauvegardée + Feuille de style sauvegardée + Feuille de style sauvegardée sans erreurs + Type de données sauvegardé + Elément de dictionnaire sauvegardé + Contenu publié + et visible sur le site + %0% documents publiés et visibles sur le site web + %0% publié et visible sur le site web + %0% documents publiés pour la langue %1% et visibles sur le site web + Contenu sauvegardé + N'oubliez pas de publier pour rendre les modifications visibles + Un planning de publication a été mis à jour + %0% sauvegardé + Envoyer pour approbation + Les modifications ont été envoyées pour approbation + %0% modifications ont été envoyées pour approbation + Media sauvegardé + Media sauvegardé sans erreurs + Membre sauvegardé + Propriété de feuille de style sauvegardée + Feuille de style sauvegardée + Modèle sauvegardé + Erreur lors de la sauvegarde de l'utilisateur (consultez les logs) + Utilisateur sauvegardé + Type d'utilisateur sauvegardé + Groupe d'utilisateurs sauvegardé + Fichier non sauvegardé + Le fichier n'a pas pu être sauvegardé. Vérifiez les permissions de fichier. + Fichier sauvegardé + Fichier sauvegardé sans erreurs + Langue sauvegardée + Type de média sauvegardé + Type de membre sauvegardé + Groupe de membres sauvegardé + Un autre groupe de membres existe déjà avec le même nom + Modèle non sauvegardé + Assurez-vous de ne pas avoir 2 modèles avec le même alias. + Modèle sauvegardé + Modèle sauvegardé sans aucune erreurs ! + Contenu publié + Variation de contenu %0% dépubliée + La langue obligatoire '%0%' a été dépubliée. Toutes les langues pour cet éléménent de contenu sont maintenant dépubliées. + Vue partielle sauvegardée + Vue partielle sauvegardée sans erreurs ! + Vue partielle non sauvegardée + Une erreur est survenue lors de la sauvegarde du fichier. + Permissions sauvegardées pour + %0% groupes d'utilisateurs supprimés + %0% a été supprimé + %0% utilisateurs activés + %0% utilisateurs désactivés + %0% est à présent activé + %0% est à présent désactivé + Les groupes d'utilisateurs ont été définis + %0% utilisateurs débloqués + %0% est à présent débloqué + Le membre a été exporté vers le fichier + Une erreur est survenue lors de l'export du membre + L'utilisateur %0% a été supprimé + Inviter l'utilisateur + L'invitation a été envoyée à nouveau à %0% + Impossible de publier le document car la langue obligatoire '%0%' n'est pas publiée + La validation a échoué pour la langue '%0%' + Le Type de Document a été exporté dans le fichier + Une erreur est survenue durant l'export du type de document + Les éléments de dictionnaire ont été exportés vers le fichier + Une erreur est survenue lors de l'export des éléments de dictionnaire. + Les éléments de dictionnaire suivants ont été importés! + La date de publication ne peut pas être dans le passé + Impossible de planifier la publication du document car la langue obligatoire '%0%' n'est pas publiée + Impossible de planifier la publication du document car la langue obligatoire '%0%' a une date de publication postérieure à celle d'une langue non obligatoire + La date d'expiration ne peut pas être dans le passé + La date d'expiration ne peut pas être antérieure à la date de publication + + + Ajouter un style + Modifier un style + Styles pour l'éditeur de texte + Definir les styles qui doivent êtres disponibles dans l'éditeur de texte pour cette feuille de style + Editer la feuille de style + Editer la propriété de feuille de style + Donner un nom pour identifier la propriété dans le Rich Text Editor + Prévisualiser + L'apparence qu'aura le text dans l'éditeur de texte. + Sélecteur + Utilise la syntaxe CSS. Ex : "h1" ou ".redHeader" + Styles + Le CSS qui devrait être appliqué dans l'éditeur de texte, e.g. "color:red;" + Code + Editeur de Texte + + + Echec de la suppression du modèle avec l'ID %0% + Editer le modèle + Sections + Insérer une zone de contenu + Insérer un placeholder de zone de contenu + Insérer + Choisissez l'élément à insérer dans votre modèle + Elément de dictionnaire + Un élément de dictionnaire est un espace pour un morceau de texte traduisible, ce qui facilite la création de designs pour des sites web multilangues. + Macro + + Une Macro est un composant configurable, ce qui est génial pour les parties réutilisables de votre + design où vous devez pouvoir fournir des paramètres, + comme les galeries, les formulaires et les listes. + + Valeur + Affiche la valeur d'un des champs de la page en cours, avec des options pour modifier la valeur ou spécifier des valeurs alternatives. + Vue partielle + + Une vue partielle est un fichier modèle séparé qui peut être à l'intérieur d'un aute modèle, + c'est génial pour réutiliser du markup ou pour séparer des modèles complexes en plusieurs fichiers. + + Modèle de base + Pas de modèle + Afficher un modèle enfant + + @RenderBody(). + ]]> + + Définir une section nommée + + @section { ... }. Celle-ci peut être affichée dans une région + spécifique du parent de ce modèle, en utilisant @RenderSection. + ]]> + + Afficher une section nommée + + @RenderSection(name). + Ceci affiche une région d'un modèle enfant qui est entourée d'une définition @section [name]{ ... } correspondante. + ]]> + + Nom de la section + La section est obligatoire + + @section, sinon une erreur est affichée. + ]]> + + Générateur de requêtes + éléments trouvés, en + Je veux + tout le contenu + le contenu du type "%0%" + à partir de + mon site web + + et + est + n'est pas + avant + avant (incluant la date sélectionnée) + après + après (incluant la date sélectionnée) + égal + n'est pas égal + contient + ne contient pas + supérieur à + supérieur ou égal à + inférieur à + inférieur ou égal à + Id + Nom + Date de Création + Date de Dernière Modification + trier par + ascendant + descendant + Modèle + + + Image + Macro + Choisissez le type de contenu + Choisissez une mise en page + Ajouter une ligne + Ajouter du contenu + Supprimer le contenu + Paramètres appliqués + Ce contenu n'est pas autorisé ici + Ce contenu est autorisé ici + Cliquez pour intégrer + Cliquez pour insérer une image + Cliquez pour insérer une macro + Ecrivez ici... + Mises en pages de la Grid + Les mises en pages représentent la surface de travail globale pour l'éditeur de grille, en général, vous n'avez seulement besoin que d'une ou deux mises en pages différentes + Ajouter une mise en page de grille + Ajustez la mise en page en définissant la largeur des colonnes et en ajoutant des sections supplémentaires + Configurations des rangées + Les rangées sont des cellules prédéfinies disposées horizontalement + Ajouter une configuration de rangée + Ajustez la rangée en réglant la largeur des cellules et en ajoutant des cellules supplémentaires + Colonnes + Nombre total combiné de colonnes dans la configuration de la grille + Paramètres + Configurez les paramètres qui peuvent être modifiés par les éditeurs + Styles + Configurez les effets de style qui peuvent être modifiés par les éditeurs + Autoriser tous les éditeurs + Autoriser toutes les configurations de rangées + Eléments maximum + Laisser vide ou mettre à 0 pour un nombre illimté + Configurer comme défaut + Choisir en plus + Choisir le défaut + ont été ajoutés + + + Compositions + Groupe + Vous n'avez pas ajouté de groupe + Ajouter un groupe + Hérité de + Ajouter une propriété + Label requis + Activer la vue en liste + Configure l'élément de contenu de manière à afficher ses éléments enfants sous forme d'une liste que l'on peut trier et filtrer, les enfants ne seront pas affichés dans l'arborescence + Modèles autorisés + Sélectionnez les modèles que les éditeurs sont autorisés à utiliser pour du contenu de ce type. + Autoriser comme racine + Autorisez les éditeurs à créer du contenu de ce type à la racine de l'arborescence de contenu. + Types de noeuds enfants autorisés + Autorisez la création de contenu des types spécifiés sous le contenu de ce type-ci + Choisissez les noeuds enfants + Hériter des onglets et propriétés d'un type de document existant. De nouveaux onglets seront ajoutés au type de document actuel, ou fusionnés s'il existe un onglet avec un nom sililaire. + Ce type de contenu est utilisé dans une composition, et ne peut donc pas être lui-même un composé. + Il n'y a pas de type de contenu disponible à utiliser dans une composition. + La suppression d'une composition supprimera les données de toutes les propriétés associées. Une fois que vous sauvegardez le type de document, il n'y a plus moyen de faire marche arrière. + Editeurs disponibles + Réutiliser + Configuration de l'éditeur + Configuration + Oui, supprimer + a été déplacé en-dessous + a été copié en-dessous + Sélectionnez le répertoire à déplacer + Sélectionnez le répertoire à copier + dans l'arborescence ci-dessous + Tous les types de document + Tous les documents + Tous les éléments media + utilisant ce type de document seront supprimés définitivement, veuillez confirmer que vous souhaitez les supprimer également. + utilisant ce type de media seront supprimés définitivement, veuillez confirmer que vous souhaitez les supprimer également. + utilisant ce type de membre seront supprimés définitivement, veuillez confirmer que vous souhaitez les supprimer également + et tous les documents utilisant ce type + et tous les éléments media utilisant ce type + et tous les membres utilisant ce type + Le membre peut éditer + Autoriser la modification de la valeur de cette propriété par le membre à partir de sa page de profil + Est une donnée sensible + Cacher cette propriété aux éditeurs de contenu qui n'ont pas accès à la visualisation des données sensibles + Afficher dans le profil du membre + Permettre d'afficher la valeur de cette propriété sur la page de profil du membre + l'onglet n'a pas d'ordre de tri + Où cette composition est-elle utilisée? + Cette composition est actuellement utilisée dans la composition des types de contenu suivants : + Permettre une variation par culture + Permettre aux éditeurs de créer du contenu de ce type dans différentes langues. + Permettre une variation par culture + Type de l'Elément + Est un Type d'Elément + Un Type d'Elément est destiné à être utilisé par exemple dans Nested Content, et pas dans l'arborescence. + Ceci n'est pas d'application pour un Type d'Elément + Vous avez apporté des modifications à cette propriété. Etes-vous certain.e de vouloir les annuler? + Nettoyer l'historique + Autoriser le remplacement des paramètres globaux de nettoyage de l'historique. + Garder toutes les versions plus récentes que jours + Garder la dernière version quotidienne pendant jours + Empêcher le nettoyage + Activer le nettoyage + REMARQUE! Le nettoyage de l'historique des versions de contenu est désactvé globalement. Ces paramètres ne prendront pas effet avant qu'il ne soit activé.]]> + + + Créer un webhook + Ajouter un header au webhook + Logs + Ajouter un Type de Document + Ajouter un Type de Media + + + Ajouter une langue + Langue obligatoire + Les propriétés doivent être remplies dans cette langue avant que le noeud ne puisse être publié. + Langue par défaut + Un site Umbraco ne peut avoir qu'une seule langue par défaut définie. + Changer la langue par défaut peut amener à ce que du contenu par défaut soit manquant. + Retombe sur + Pas de langue alternative + Pour permettre à un site multi-langue de retomber sur une autre langue dans le cas où il n'existe pas dans la langue demandée, sélectionnez-là ici. + Langue alternative + aucune + + + Ajouter un paramètre + Modifier le paramètre + Introduire le nom de la macro + Paramètres + Définir les paramètres qui devraient être disponibles lorsque l'on utilise cette macro. + Sélectionner le fichier de vue partielle de la macro + + + Fabrication des modèles + ceci peut prendre un peu de temps, ne vous inquiétez pas + Modèles générés + Les modèles n'ont pas pu être générés + La génération des modèles a échoué, voyez les exceptions dans les U log + + + Ajouter une valeur par défaut + Valeur par défaut + Champ alternatif + Texte alternatif + Casse + Encodage + Choisir un champ + Convertir les sauts de ligne + Remplace les sauts de ligne avec des balises 'br' + Champs particuliers + Oui, la date seulement + Formater comme une date + Encoder en HTML + Remplacera les caractères spéciaux par leur équivalent HTML. + Sera inséré après la valeur du champ + Sera inséré avant la valeur du champ + Minuscules + Aucun + Example de résultat + Insérer après le champ + Insérer avant le champ + Récursif + Oui, rendre récursif + Champs standards + Majuscules + Encode pour URL + Formatera les caractères spéciaux dans les URL + Sera seulement utilisé si toutes les valeurs des champs ci-dessus sont vides + Ce champ sera utilisé seulement si le champ initial est vide + Oui, avec l'heure. Séparateur: + + + Détails + Télécharger la DTD XML + Champs + Inclure les pages enfants + + + + Aucun utilisateur traducteur trouvé. Veuillez créer un utilisateur traducteur avant d'envoyer du contenu pour traduction + La page '%0%' a été envoyée pour traduction + Envoyer la page '%0%' pour traduction + Nombre total de mots + Traduire en + Traduction complétée. + Vous pouvez prévisualiser les pages que vous avez traduites en cliquant ci-dessous. Si la page originale est trouvée, vous verrez une comparaison entre les deux pages. + Traduction échouée, il se pourrait que fichier XML soit corrompu + Options de traduction + Traducteur + Uploader le fichier de traduction XML + + + Contenu + Types de contenu + Media + Navigateur de cache + Corbeille + Packages créés + Types de données + Dictionnaire + Packages installés + Installer une skin + Installer un starter kit + Langues + Installer un package local + Macros + Types de média + Membres + Groupes de membres + Rôles + Types de membres + Types de documents + Types de relations + Packages + Packages + Vues Partielles + Vues Partielles pour les Fichiers Macro + Installer depuis le repository + Installer Runway + Modules Runway + Fichiers de script + Scripts + Feuilles de style + Modèles + Visualisation des Log + Utilisateurs + Configuration + Modélisation + Parties Tierces + Webhooks + + + Nouvelle mise à jour disponible + %0% est disponible, cliquez ici pour télécharger + Aucune connexion au serveur + Erreur lors de la recherche de mises à jour. Veuillez vérifier le stack trace pour obtenir plus d'informations. + + + Accès + Sur base des groupes et des noeuds de départ, l'utilisateur a accès aux noeuds suivants + Donner accès + Administrateur + Champ catégorie + Utilisateur créé + Changer le mot de passe + Changer la photo + Nouveau mot de passe + Plus que %0% caractère(s) minimum! + Il devrait y avoir au moins %0% caractère(s) spéciaux. + n'a pas été bloqué + Le mot de passe n'a pas été modifié + Confirmez votre nouveau mot de passe + Vous pouvez changer votre mot de passe d'accès au backoffice Umbraco en remplissant le formulaire ci-dessous puis en cliquant sur le bouton "Changer le mot de passe" + Canal de contenu + Créer un autre utilisateur + Créer de nouveaux utilisateurs pour leur donner accès à Umbraco. Lors de la création d'un nouvel utilisateur, un mot de passe est généré que vous pouvez partager avec ce dernier. + Champ description + Désactiver l'utilisateur + Type de document + Editeur + Champ extrait + Tentatives de connexion échouées + Voir le profil de l'utilisateur + Ajouter des groupes pour donner les accès et permissions + Inviter un autre utilisateur + Inviter de nouveaux utilisateurs pour leur donner accès à Umbraco. Un email d'invitation sera envoyé à chaque utilisateur avec des informations concernant la connexion à Umbraco. Les invitations sont valables pendant 72 heures. + Langue + Spécifiez la langue dans laquelle vous souhaitez voir les menus et dialogues + Date du dernier bloquage + Dernière connexion + Dernière modification du mot de passe + Identifiant + Noeud de départ dans la librarie de média + Limiter la librairie média à un noeud de départ spécifique + Noeuds de départ dans la librairie de média + Limiter la librairie média à des noeuds de départ spécifique + Sections + Désactiver l'accès Umbraco + ne s'est pas encore connecté + Ancien mot de passe + Mot de passe + Réinitialiser le mot de passe + Votre mot de passe a été modifié! + Veuillez confirmer votre nouveau mot de passe + Introduisez votre nouveau mot de passe + Votre nouveau mot de passe ne peut être vide ! + Mot de passe actuel + Mot de passe actuel invalide + Il y a une différence entre le nouveau mot de passe et le mot de passe confirmé. Veuillez réessayer. + Le mot de passe confirmé ne correspond pas au nouveau mot de passe saisi! + Remplacer les permissions sur les noeuds enfants + Vous êtes en train de modifiez les permissions pour les pages : + Choisissez les pages dont les permissions doivent être modifiées + Supprimer la photo + Permissions par défaut + Permissions granulaires + Définir les permissions sur des noeuds spécifiques + Profil + Rechercher tous les enfants + Ajouter les sections auxquelles les utilisateurs peuvent accéder + Sélectionner les groupes d'utilisateurs + Aucun noeud de départ sélectionné + Aucun noeud de départ sélectionné + Noeud de départ du contenu + Limiter l'arborescence de contenu à un noeud de départ spécifique + Noeuds de départ du contenu + Limiter l'arborescence de contenu à des noeuds de départ spécifiques + Dernière mise à jour de l'utilisateur + a été créé + Le nouvel utilisateur a été créé avec succès. Utilisez le mot de passe ci-dessous pour la connexion à Umbraco. + Gestion des utilisateurs + Nom d'utilisateur + Permissions de l'utilisateur + Groupe d'utilisateurs + a été invité + Une invitation a été envoyée au nouvel utilisateur avec les détails concernant la connexion à Umbraco. + Bien le bonjour et bienvenue dans Umbraco! Vous serez prêt.e dans moins d'1 minute, vous devez encore simplement configurer votre mot de passe. + Bienvenue dans Umbraco! Malheureusement, votre invitation a expiré. Veuillez contacter votre administrateur et demandez-lui de vous l'envoyer à nouveau. + Rédacteur + Modifier + Votre profil + Votre historique récent + La session expire dans + Inviter un utilisateur + Créer un utilisateur + Envoyer l'invitation + Retour aux utilisateurs + Umbraco: Invitation + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Salut %0%, +

+

+ Vous avez été invité.e par %1% à accéder au Umbraco Back Office. +

+

+ Message de %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Cliquez sur ce lien pour accepter l'invitation + +
+
+

Si vous ne pouvez pas cliquer sur le lien, copiez cet URL dans votre navigateur :

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + ]]> +
+ Nouvel envoi de l'invitation en cours... + Supprimer l'Utilisateur + Etes-vous certain(e) de vouloir supprimer le compte de cet utilisateur? + Tous + Actif + Désactivé + Bloqué + Invité + Inactif + Nom (A-Z) + Nom (Z-A) + Plus ancien + Plus récent + Dernière connexion + + + Validation + Valider comme email + Valider comme nombre + Valider comme URL + ...ou introduisez une validation spécifique + Champ obligatoire + Introduisez un message d'erreur de validation personnalisé (optionnel) + Introduisez une expression régulière + Introduisez un message d'erreur de validation personnalisé (optionnel) + Vous devez ajouter au moins + Vous ne pouvez avoir que + éléments + éléments sélectionnés + Date non valide + Pas un nombre + Email non valide + La valeur ne peut pas être null + La valeur ne peut pas être vide + Valeur non valide, elle ne correspond pas au modèle correct + Validation personnalisée + %1% supplémentaires.]]> + %1% en trop.]]> + + + + La valeur est égale à la valeur recommandée : '%0%'. + La valeur attendue pour '%2%' dans le fichier de configuration '%3%' est '%1%', mais la valeur trouvée est '%0%'. + La valeur inattendue '%0%' a été trouvée pour '%2%' dans le fichier de configuration '%3%'. + + MacroErrors est fixé à la valeur '%0%'. + MacroErrors est fixé à la valeur '%0%', ce qui empêchera certaines ou même toutes les pages de votre site de se charger complètement en cas d'erreur dans les macros. La rectification de ceci fixera la valeur à '%1%'. + + + Le certificat de votre site a été marqué comme valide. + Erreur de validation du certificat : '%0%' + Le certificat SSL de votre site web a expiré. + Le certificat SSL de votre site web va expirer dans %0% jours. + Erreur en essayant de contacter l'URL %0% - '%1%' + Vous êtes actuellement %0% à voir le site via le schéma HTTPS. + + L'appSetting 'Umbraco:CMS:Global:UseHttps' se trouve à 'false' dans votre fichier appSettings.json. + Une fois que vous aurez accès à ce site via le schema HTTPS, il faudra la mettre à 'true'. + + + L'appSetting 'Umbraco:CMS:Global:UseHttps' se trouve à '%0%' dans votre fichier + appSettings.json, vos cookies sont %1% marqués comme 'secured'. + + + Le mode de compilation Debug est désactivé. + Le mode de compilation Debug est actuellement activé. Il est recommandé de désactiver ce paramètre avant la mise en ligne. + + + + X-Frame-Options, utilisé pour contrôler si un site peut être intégré dans un autre via IFRAME, a été trouvé.]]> + X-Frame-Options , utilisé pour contrôler si un site peut être intégré dans un autre via IFRAME, n'a pas été trouvé.]]> + X-Content-Type-Options utilisé pour la protection contre les vulnérabilités de MIME sniffing a été trouvé.]]> + X-Content-Type-Options utilisé pour la protection contre les vulnérabilités de MIME sniffing n'a pas été trouvé.]]> + Strict-Transport-Security, aussi connu sous le nom de HSTS-header, a été trouvé.]]> + Strict-Transport-Security, aussi connu sous le nom de HSTS-header, n'a pas été trouvé.]]> + X-XSS-Protection a été trouvé.]]> + X-XSS-Protection n'a pas été trouvé.]]> + + %0%.]]> + Aucun header révélant des informations à propos de la technologie du site web n'a été trouvé. + La configuration SMTP est correcte et le service fonctionne comme prévu. + %0%.]]> + %0%.]]> +

Les résultats de l'exécution du Umbraco Health Checks planifiée le %0% à %1% sont les suivants :

%2%]]>
+ Statut du Umbraco Health Check: %0% + + + Désactiver URL tracker + Activer URL tracker + Culture + URL original + Redirigé Vers + Gestion des redirections d'URL + Les URLs suivants redirigent vers cet élément de contenu : + Aucune redirection n'a été créée + Lorsqu'une page publiée est renommée ou déplacée, une redirection sera automatiquement créée vers la nouvelle page. + Redirection d'URL supprimée. + Erreur lors de la suppression de la redirection d'URL. + Ceci supprimera la redirection + Etes-vous certain(e) de vouloir désactiver le URL tracker? + URL tracker est maintenant désactivé. + Erreur lors de la désactivation de l'URL tracker, plus d'information disponible dans votre fichier log. + URL tracker est maintenant activé. + Erreur lors de l'activation de l'URL tracker, plus d'information disponible dans votre fichier log. + + + Pas d'élément de dictionaire à choisir + + + %0% caractères restant.]]> + %1% en trop.]]> + + + Suppression du contenu avec l'Id : {0} lié au contenu parent original avec l'Id : {1} + Suppression du media avec l'Id : {0} lié à l'élément media parent original avec l'Id : {1} + Cet élément ne peut pas être restauré automatiquement + Il n'y a aucun endroit où cet élément peut être restauré automatiquement. Vous pouvez déplacer l'élément manuellement en utilisant l'arborescence ci-dessous. + a été restauré sous + + + Direction + Parent vers enfant + Bi-directionnel + Parent + Enfant + Nombre + Relations + Création + Remarque + Nom + Aucune relation pour ce type de relation. + Type de Relation + Relations + + + Pour Commencer + Gestion des redirections d'URL + Contenu + Bienvenue + Gestion d'Examine + Statut Publié + Models Builder + Health Check + Profilage + Pour Commencer + Installer Umbraco Forms + + + Retour + Layouts actifs : + Aller à + groupe + passé + avertissement + échoué + suggestion + Vérifier les succès + Vérifier les échecs + Ouvrir la recherche backoffice + Ouvrir/Fermer l'aide backoffice + Ouvrir/Fermer vos options de profil + Ouvrir le menu de contexte pour + Langue actuelle + Changer la langue vers + Créer un nouveau dossier + Partial View + Macro de Partial View + Membre + Chercher dans l'arborescence de contenu + Quantité maximum + Afficher les éléments enfant pour + Ouvrir le noeud de contexte pour + + + Références + Ce Type de Données n'a pas de références. + Utilisé dans des Types de Document + Utilisé dans les Types de Media + Utilisé dans les Types de Membre + Utilisé par + + + Niveaux de Log + Tout sélectionner + Tout déselectionner + Recherches sauvegardées + Nombre total d'éléments + Date + Niveau + Machine + Message + Exception + Propriétés + Chercher avec Google + Chercher ce message avec Google + Chercher avec Bing + Chercher ce message avec Bing + Chercher dans Our Umbraco + Chercher ce message dans les forums et docs de Our Umbraco + Chercher dans Our Umbraco avec Google + Chercher dans les forums de Our Umbraco en utilisant Google + Chercher dans les Sources Umbraco + Chercher dans le code source d'Umbraco sur Github + Chercher dans les Umbraco Issues + Chercher dans les Umbraco Issues sur Github + Supprimer cette recherche + Trouver les Logs avec la Request ID + Trouver les Logs avec le Namespace + Trouver les logs avec le Nom de Machine + Ouvrir + + + Copier %0% + %0% de %1% + Supprimer tous les éléments + + + Ouvrir les Property Actions + + + Rafraîchir le Statut + Cache Mémoire + + + + Recharger + Cache en Base de Données + + La reconstruction peut être une opération lourde. + Utilisez-le lorsque le rechargement ne suffit pas, et que vous pensez que la cache en base de données n'a pas été + générée convenablement—ce qui indiquerait des problèmes critiques dans Umbraco. + ]]> + + Reconstruire + Opérations Internes + + pas besoin de l'utiliser. + ]]> + + Collecter + Statut de la Cache Publiée + Caches + + + Profilage de performances + + + Umbraco est actuellement exécuté en mode debug. Cela signifie que vous pouvez utiliser le profileur de performances intégré pour évaluer les performance lors du rendu des pages. +

+

+ Si vous souhaitez activer le profileur pour le rendu d'une page spécifique, ajoutez simplement umbDebug=true au querystring lorsque vous demandez la page. +

+

+ Si vous souhaitez que le profileur soit activé par défaut pour tous les rendus de pages, vous pouvez utiliser le bouton bascule ci-dessous. + Cela créera un cookie dans votre browser, qui activera alors le profileur automatiquement. + En d'autres termes, le profileur ne sera activé par défaut que dans votre browser - pas celui des autres. +

+ ]]> +
+ Activer le profileur par défaut + Rappel amical + + + Des heures de vidéos de formation Umbraco ne sont qu'à un clic d'ici + + Vous voulez maîtriser Umbraco? Passez quelques minutes à apprendre certaines des meilleures pratiques en regardant une de ces vidéos à propos de l'utilisation d'Umbraco. Et visitez umbraco.tv pour encore plus de vidéos Umbraco

+ ]]> +
+ Pour démarrer + + + Commencer ici + Cette section contient les blocs fondamentaux pour votre site Umbraco. Suivez les liens ci-dessous pour en apprendre d'avantage sur la façon de travailler avec les éléments de la section Settings + En savoir plus + + dans la section Documentation de Our Umbraco + ]]> + + + Community Forum + ]]> + + + tutoriels vidéos (certains sont gratuits, certains nécessitent un abonnement) + ]]> + + + outils d'amélioration de productivité et notre support commercial + ]]> + + + formations et certifications + ]]> + + + + élément retrouvé + éléments retrouvés + +
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/it_ch.xml b/src/Umbraco.Core/EmbeddedResources/Lang/it_ch.xml new file mode 100644 index 0000000000..08de0a51be --- /dev/null +++ b/src/Umbraco.Core/EmbeddedResources/Lang/it_ch.xml @@ -0,0 +1,3170 @@ + + + + The Umbraco community + https://docs.umbraco.com/umbraco-cms/extending/language-files + + + Gestisci hostnames + Audit Trail + Sfoglia + Cambia tipo di documento + Cambia tipo di dato + Copia + Crea + Esporta + Crea pacchetto + Crea gruppo + Cancella + Disabilita + Modifica impostazioni + Svuota il cestino + Abilita + Esporta il tipo di documento + Importa il tipo di documento + Importa il pacchetto + Modifica in Area di Lavoro + Uscita + Sposta + Notifiche + Accesso pubblico + Pubblica + Non pubblicare + Aggiorna + Ripubblica intero sito + Rimuovi + Rinomina + Ripristina + Imposta i permessi per la pagina %0% + Scegli dove copiare + Scegli dove muovere + nella struttura sottostante + Scegli dove copiare l'oggetto/gli oggetti selezionati + Scegli dove spostare l'oggetto/gli oggetti selezionati + + + + Permessi + Annulla ultima modifica + Invia per la pubblicazione + Invia per la traduzione + Crea gruppo + Ordina + Traduci + Aggiorna + Imposta permessi + Sblocca + Crea modello di contenuto + Invia nuovamente l'invito + + + Contenuto + Amministrazione + Struttura + Altro + + + Consenti l'accesso per assegnare gli hostnames + Consenti l'accesso per visualizzare la cronologia di un nodo + Consenti l'accesso per visualizzare un nodo + Consenti l'accesso per cambiare tipo di documento a un nodo + Consenti l'accesso per copiare un nodo + Consenti l'accesso per creare i nodi + Consenti l'accesso per eliminare i nodi + Consenti l'accesso per spostare un nodo + Consenti l'accesso per impostare e cambiare le restrizioni di accesso a un nodo + Consenti l'accesso per pubblicare un nodo + Consenti l'accesso per non pubblicare un nodo + Consenti l'accesso per cambiare i permessi di un nodo + Consenti l'accesso per riportare un nodo a una versione precedente + Consenti l'accesso per inviare un nodo in approvazione prima di pubblicare + Consenti l'accesso per inviare un nodo per la traduzione + Consenti l'accesso per cambiare l'ordinamento dei nodi + Consenti l'accesso per tradurre un nodo + Consenti l'accesso per salvare un nodo + Consenti l'accesso per creare un modello di contenuto + + + Contenuto + Info + + + Permesso negato. + Aggiungi nuovo dominio + rimuovi + Nodo non valido. + + + Lingua + Dominio + + + + + Modifica il dominio corrente + + + + Eredita + Lingua + + oppure eredita la lingua dai nodi padre. Si applicherà anche
+ al nodo corrente, a meno che un dominio sotto non venga applicato.]]> +
+ Domini + + + Cancella selezione + Seleziona + Fai qualcos'altro + Grassetto + Cancella rientro paragrafo + Inserisci campo del form + Inserisci intestazione grafica + Modifica Html + Inserisci rientro paragrafo + Corsivo + Centra + Allinea testo a sinistra + Allinea testo a destra + Inserisci Link + Inserisci local link (ancora) + Elenco puntato + Elenco numerato + Inserisci macro + Inserisci immagine + Pubblica e chiudi + Pubblica con discendenti + Modifica relazioni + Ritorna alla lista + Salva + Salva e chiudi + Salva e pubblica + Salva e pianifica + Salva e invia per approvazione + Salva list view + Pianifica + Anteprima + Salva e visualizza anteprima + + Scegli stile + Vedi stili + Inserisci tabella + Genera modelli e chiudi + Salva e genera modelli + Indietro + Avanti + Ripristina + Elimina tag + Cancella + Conferma + + Invia + Invia e chiudi + + + Media eliminato + Media spostato + Media copiato + Media salvato + + + Visualizzazione per + Contenuto eliminato + Contenuto non pubblicato + Contenuto non pubblicato per le lingue: %0% + Contenuto pubblicato + Contenuto pubblicato per le lingue: %0% + Contenuto salvato + Contenuto salvato per le lingue: %0% + Contenuto spostato + Contenuto copiato + Contenuto ripristinato + Contenuto inviato per l'approvazione + Contenuto inviato per l'approvazione per le lingue: %0% + Ordina gli elementi figlio eseguito dall'utente + %0% + Copia + Pubblica + Pubblica + Sposta + Salva + Salva + Elimina + Non pubblicare + Non pubblicare + Ripristina + Invia per l'approvazione + Invia per l'approvazione + Ordina + Personalizzato + Cronologia (tutte le varianti) + + + Impossibile creare una cartella sotto genitore con ID %0% + Impossibile creare una cartella sotto genitore con nome %0% + + + + Impossibile eliminare l'elemento: %0% + + + Pubblicato + Informazioni su questa pagina + Alias + (come descriveresti l'immagine via telefono) + Links alternativi + Clicca per modificare questo elemento + Creato da + Autore originale + Aggiornato da + Creato il + + Tipo di documento + Modifica + Attivo fino al + + + Ultima pubblicazione + Non ci sono elementi da visualizzare + Non ci sono elementi da visualizzare nella lista. + + + Tipo di media + Link ai media + Gruppo di membri + Ruolo + Tipo di Membro + Non sono state effettuate modifiche + + Titolo della Pagina + Questo media non ha link + + + + + + + + + + Impossibile ottenere l'URL + + + + + + + Pubblica + Pubblicato + Pubblicato (modifiche in sospeso) + Stato della pubblicazione + + %0% e tutti gli elementi sottostanti, rendendo così il loro contenuto pubblicamente disponibile.]]> + + + + + Pubblicato il + Non pubblicato il + Rimuovi data + Imposta data + Ordinamento dei nodi aggiornato + + + + Statistiche + Titolo (opzionale) + Testo alternativo (opzionale) + Didascalia (opzionale) + Tipo + Non pubblicare + Bozza + Non creato + Ultima modifica + + Rimuovi file(s) + Clicca qui per rimuovere l'immagine dal media + Clicca qui per rimuovere il file dal media + Link al documento + Membro del gruppo/i + Non un membro del gruppo/i + Elementi figli + Target + Questo si traduce nella seguente ora sul server: + + Cosa significa questo?]]> + + Sei sicuro di voler eliminare questo oggetto? + Sei sicuro di voler eliminare tutti gli oggetti? + + + + + + + Aggiungi Element Type + Seleziona Element Type + + + + + Immettere un'espressione di angular da valutare rispetto a ciascun + elemento per il relativo nome. Utilizza + + per visualizzare l'index dell'oggetto + Aggiungi un altro box di testo + Rimuovi questa text box + Root del contenuto + Includi elementi di contenuto non pubblicati. + + + + + + Quali lingue vorresti pubblicare? Tutte le lingue con contenuto vengono + salvate! + + Quali lingue vorresti pubblicare? + Quali lingue vorresti salvare? + Tutte le lingue con contenuto vengono salvate alla creazione! + Quali lingue vorresti inviare per l'approvazione? + Quali lingue vorresti pianificare? + + Seleziona le lingue da non pubblicare. Non pubblicando una lingua obbligatoria + annullerai la pubblicazione di tutte le lingue. + + Lingue pubblicate + Lingue non pubblicate + Lingue non modificate + Queste lingue non sono state create. + + Tutte le nuove varianti verranno salvate. + Quali varianti vorresti pubblicare? + Scegli quali varianti verranno salvate. + Scegli le varianti da inviare per l'approvazione. + Imposta pubblicazione pianificata... + + Seleziona le varianti da non pubblicare. Non pubblicando una lingua obbligatoria + annullerai la pubblicazione di tutte le varianti. + + Per la pubblicazione sono necessarie le seguenti varianti: + + Non siamo pronti per la pubblicazione + Pronto per la pubblicazione? + Pronto per il salvataggio? + Invia per l'approvazione + Seleziona da data e l'ora in cui pubblicare/non pubblicare il contenuto. + Crea nuovo/a + Incolla dagli appunti + + + + Crea un nuovo modello di contenuto da '%0%' + Vuoto + Seleziona un modello di contenuto + Modello di contenuto creato + + + + + + + + Clicca per caricare + o clicca qui per scegliere i files + Puoi trascinare i file qui per caricarli. + Impossibile caricare questo file, non ha un tipo di file approvato + + Media root + Impossibile spostare il media + La cartella padre e di destinazione non possono essere le stesse + Impossibile copiare il media + Impossibile creare una cartella sotto l'id padre %0% + Impossibile rinominare la cartella con id %0% + Trascina e rilascia i tuoi file nell'area + + + + Crea un nuovo membro + Tutti i membri + + + + + + Impossibile copiare il content type + Impossibile spostare il content type + + + Impossibile copiare il media type + Impossibile spostare il media type + Selezione automatica + + + Impossibile copiare il member type + + + + Crea un elemento sotto + Seleziona il Document Type per cui vuoi creare un modello di contenuto + Inserisci il nome della cartella + Scegli il tipo ed il titolo + + Tipi di documento dentro la sezione Impostazioni, modificando Tipi di nodi figlio consentiti sotto Permessi.]]> + + + Tipi di documento dentro la sezione Impostazioni.]]> + + + La pagina selezionata nel content tree non permette a nessuna + pagina di essere creata sotto di essa. + + Modifica permessi per questo tipo di documento + Crea un nuovo tipo di documento + + Tipi di documento dentro la sezione Impostazioni, cambiando l'opzione Consenti come root sotto Permessi.]]> + + + Tipi di documento dentro la sezione Impostazioni, modificando Tipi di nodi figlio consentiti sotto Permessi.]]> + + + Il media selezionato non consente la creazione di altri media al di + sotto di esso. + + Modifica permessi per questo tipo di media + Tipo di documento senza template + Tipo di documento con template + + + + Tipo di documento + + + + Tipo di elemento + + + + Composizione + + + + Cartella + + Utilizzato per organizzare i tipi di documento, le composizioni e i tipi di elementi + creati in questo albero dei tipi di documento. + + Nuova cartella + Nuovo tipo di dato + Nuovo file JavaScript + Nuova partial view vuota + Nuova partial view macro + Nuova partial view da snippet + Nuova partial view macro da snippet + Nuova partial view macro (senza macro) + Nuovo foglio di stile + Nuovo foglio di stile per Rich Text Editor + + + + + + + + hai aperto una nuova finestra + Riavvia + Visita + Benvenuto + + + Rimani + Scarta le modifiche + Hai delle modifiche non salvate + + Sei sicuro di voler lasciare questa pagina? Hai delle modifiche non salvate! + + Pubblicando renderai visibli gli oggetti selezionati sul sito. + + Non pubblicando rimuoverai gli oggetti selezionati e i loro discendenti dal + sito. + + Non pubblicando rimuoverai questa pagina e tutti i suoi discendenti dal sito. + + + + + + Fatto + Eliminato %0% elemento + Eliminati %0% elementi + Eliminato %0% su %1% elemento + Eliminati %0% su %1% elementi + Pubblicato %0% elemento + Pubblicati %0% elementi + Pubblicato %0% su %1% elemento + Pubblicati %0% su %1% elementi + %0% elemento non pubblicato + %0% elementi non pubblicati + Elementi non pubblicati: %0% su %1% + Elementi non pubblicati: %0% su %1% + Spostato %0% elemento + Spostati %0% elementi + Spostato %0% su %1% elemento + Spostati %0% su %1% elementi + Copiato %0% elemento + Copiati %0% elementi + Copiato %0% su %1% elemento + Copiati %0% su %1% elementi + + + Titolo del Link + Link + Ancora / querystring + Nome + Chiudi questa finestra + Sei sicuro di voler eliminare + Sei sicuro di voler disabilitare + Sei sicuro di voler rimuovere + %0%]]> + %0%]]> + + + Taglia + Modifica elemento del Dizionario + Modifica la lingua + Modifica il media selezionato + Inserisci il link locale + Inserisci carattere + Inserisci intestazione grafica + Inserisci immagine + Inserisci link + Inserisci macro + Inserisci tabella + + + + + Ultima modifica + Link + + + + Impostazioni della macro + + Incolla + Modifica i permessi per + Imposta i permessi per + Imposta i permessi per %0% per il gruppo di utenti %1% + Seleziona i gruppi di utenti per il quale vuoi impostare i permessi + + + + + + + regexlib.com ha attualmente qualche problema, di cui non abbiamo il controllo. Siamo spiacevoli dell'inconveniente.]]> + + + + + Elimina Macro + Campo obbligatorio + + + + + + Numero di colonne + Numero di righe + + Seleziona elemento + Visualizza gli elementi in cache + Relaziona con l'originale + Includi discendenti + + Link alla pagina + Apre il documento linkato in una nuova finestra o tab + Link al media + Seleziona il nodo di inizio per il contenuto + Seleziona media + Seleziona tipo di media + Seleziona icona + Seleziona oggetto + Seleziona link + Seleziona macro + Seleziona contenuto + Seleziona tipo di contenuto + Seleziona il nodo di inizio per i media + Seleziona membro + Seleziona gruppo di membri + Seleziona tipo di membri + Seleziona nodo + Seleziona sezioni + Seleziona utente + Seleziona utenti + Non sono state trovate icone + Non ci sono parametri per questa macro + Non ci sono macro disponibili da inserire + Provider di accesso esterni + Exception Details + Stacktrace + Inner Exception + Linka il tuo + Togli il link al tuo + account + Seleziona editor + Seleziona configurazione + Seleziona snippet + + + + %0%.]]> + %0% dal gruppo %1%]]> + Si, rimuovi + + + Non ci sono oggetti nel Dizionario. + + + %0%' qui sotto.]]> + Nome della cultura + + Panoramica del Dizionario + + + Searchers configurati + + + + Valori del campo + Stato di salute + + Indexers + Index info + + Gestisci gli indexes di Examine + + Permette di visualizzare i dettagli di ogni index e fornisce alcuni strumenti + per gestire gli index + + Ricostruisci index + + + A seconda della quantità di contenuti presenti nel tuo sito, potrebbe volerci un po' di tempo.
+ Non è consigliabile ricostruire un indice durante i periodi di elevato traffico del sito Web o quando gli editor modificano i contenuti. + ]]> +
+ Searchers + Cerca nell'index e visualizza i risultati + Strumenti + Strumenti per gestire l'index + Campi + + + + + + IIndexPopulator + + + Inserisci il tuo username + Inserisci la tua password + Conferma la tua password + Dai un nome a %0%... + Inserisci un nome... + Inserisci un email... + Inserisci un username... + Etichetta... + Inserisci una descrizione... + Cerca... + Filtra... + Scrivi per aggiungere tags (premi invio dopo ogni tag)... + Inserisci la tua email + Inserisci un messaggio... + + #value oppure ?key=value + Inserisci un alias... + Sto generando l'alias... + Crea oggetto + Modifica + Nome + + + Crea una list view custom + Rimuovi la list view custom + + + + + + Rinominato + Inserisci qui il nuovo nome della cartella + + + + + + + Rendering controllo + Bottoni + Abilita impostazioni avanzate per + Abilita menu contestuale + Dimensione massima delle immagini inserite + Fogli di stile collegati + Visualizza etichetta + Larghezza e altezza + + + + + Si, elimina + + + + Seleziona la cartella da spostare + nella struttura sottostante + + + %0% eliminerà le proprietà e i dati dagli oggetti seguenti]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Errore durante il caricamento di una Partial View script (file: %0%) + + + + + + + + + + + + + + + + + + + + Opzioni + Info + Azione + Azioni + Aggiungi + Alias + Tutti + + Indietro + Torna alla panoramica + Bordo + o + Annulla + + Scegli + Pulisci + Chiudi + Chiudi la finestra + Chiudi il pannello + Commento + Conferma + Vincola + Vincola le proporzioni + Contenuto + Continua + Copia + Crea + Selezione di ritaglio + Database + Data + Default + Elimina + Eliminato + Eliminazione... + Design + Dizionario + Dimensioni + Scarta + + Scarica + Modifica + Modificato + Elementi + Email + Errore + Campo + Trova + Primo + Punto focale + Generale + Gruppi + Gruppo + Altezza + Guida + Nascondi + Cronologia + Icona + Id + Importa + Includi le sottocartelle nella ricerca + Cerca solo in questa cartella + Info + + Inserisci + Installa + Non valido + Giustificato + Etichetta + Lingua + Ultimo + Layout + Links + Caricamento + Bloccato + Login + Log off + Logout + Macro + Obbligatorio + Messaggio + Sposta + Nome + Nuovo + Successivo + No + di + Off + Ok + Apri + On + o + Ordina per + Password + Percorso + + Precedente + + Ricompila + + Cestino + + Ricarica + Rimangono + Rimuovi + Rinomina + Rinnova + Obbligatorio + Recupera + Riprova + Permessi + Pubblicazione programmata + Cerca + Spiacenti, non riusciamo a trovare quello che stai cercando. + + Server + Impostazioni + Mostra + Mostra la pagina inviata + Dimensione + Ordina + Stato + Conferma + Riuscito + Tipo + Digita per cercare... + sotto + Su + Aggiorna + Aggiornamento + Carica + URL + Utente + + Valore + Vedi + Benvenuto... + Larghezza + Si + Cartella + Risultati di ricerca + Riordina + Ho finito di riordinare + Anteprima + Cambia password + a + Vista lista + Salvataggio... + corrente + Incorpora + selezionato + Altro + Articoli + Video + Installazione + Avatar per + + + Blu + + + Aggiungi gruppo + + Aggiungi editor + Aggiungi template + Aggiungi nodo figlio + Aggiungi figlio + Modifica tipo di dato + Naviga tra le sezioni + Scorciatoie + vedi scorciatoie + Attiva/disattiva vista lista + Attiva/disattiva consenti come root + Commenta/Non commentare righe + Rimuovi riga + Copia linee sopra + Copia linee sotto + Sposta linee in su + + Generale + Editor + Attiva/disattiva consenti varianti lingua + Attiva/disattiva la segmentazione + + + Colore di sfondo + Grassetto + Colore del testo + Carattere + Testo + + + Pagina + + + + + + + installa per installare il database Umbraco %0% + ]]> + + + Avanti per proseguire.]]> + + + + Se è necessario contatta il tuo ISP per reperire le informazioni necessarie. + Se stai effettuando l'installazione in locale o su un server, puoi richiederle al tuo amministratore di sistema.]]> + + + + Premi il tasto aggiorna per aggiornare il database ad Umbraco %0%

+

+ Non preoccuparti, il contenuto non verrà perso e tutto continuerà a funzionare dopo l'aggiornamento! +

+ ]]> +
+ + Premi il tasto Avanti per + continuare. ]]> + + + Avanti per continuare la configurazione.]]> + + + La password predefinita per l'utente di default deve essere cambiata!]]> + + + L'utente di default è stato disabilitato o non ha accesso ad Umbraco!

Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> + + + La password dell'utente di default è stata modificata con successo

Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> + + + + + + + + + + + + + La configurazione dei permessi è quasi perfetta!

+ Puoi eseguire Umbraco senza problemi, ma non sarai in grado di installare i pacchetti che sono consigliati per sfruttare a pieno le potenzialità di Umbraco.]]> +
+ + + + video tutorial su come impostare i permessi delle cartelle per Umbraco o leggi la versione testuale.]]> + + + Le impostazioni dei permessi potrebbero avere dei problemi! +

+ Puoi eseguire Umbraco senza problemi, ma non sarai in grado di creare cartelle o installare pacchetti che sono consigliati per sfruttare a pieno le potenzialità di Umbraco.]]> +
+ + Le impostazioni dei permessi non sono corrette per Umbraco! +

+ Per eseguire Umbraco, devi aggiornare le impostazioni dei permessi.]]> +
+ + La configurazione dei permessi è perfetta!

+ Sei pronto per avviare Umbraco e installare i pacchetti!]]> +
+ + + + + + + + + + + Guarda come) + Puoi anche installare eventuali Runway in un secondo momento. Vai nella sezione Developer e scegli Pacchetti. + ]]> + + + + + + + + Questa è la lista dei nostri moduli raccomandati, seleziona quali vorresti installare, o vedi l'intera lista di moduli + ]]> + + Raccommandato solo per utenti esperti + Vorrei iniziare da un sito semplice + + + "Runway" è un semplice sito web contenente alcuni tipi di documento e alcuni templates di base. L'installer configurerà Runway per te automaticamente, + ma tu potrai facilmente modificarlo, estenderlo o eliminarlo. Non è necessario installarlo e potrai usare Umbraco anche senza di esso, ma + Runway ti offre le basi e le best practices per cominciare velocemente. + Se sceglierai di installare Runway, volendo potrai anche selezionare i moduli di Runway per migliorare le pagine. +

+ + Inclusi in Runway: Home page, pagina Guida introduttiva, pagina Installazione moduli
+ Moduli opzionali: Top Navigation, Sitemap, Contatti, Gallery. +
+ ]]> +
+ + Passo 1/5 Accettazione licenza + Passo 2/5: Configurazione database + Passo 3/5: Controllo permessi dei file + Passo 4/5: Controllo impostazioni sicurezza + + Grazie per aver scelto Umbraco + + Naviga per il tuo nuovo sito +Hai installato Runway, quindi perché non dare uno sguardo al vostro nuovo sito web.]]> + + + Ulteriori informazioni e assistenza +Fatti aiutare dalla nostra community, consulta la documentazione o guarda alcuni video gratuiti su come costruire un semplice sito web, come usare i pacchetti e una guida rapida alla terminologia Umbraco]]> + + + + iniziare immediatamente cliccando sul bottone "Avvia Umbraco".
Se sei nuovo su Umbraco, + si possono trovare un sacco di risorse sulle nostre pagine Getting Started.]]> +
+ + Avvia Umbraco +Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e iniziare ad aggiungere i contenuti, aggiornando i modelli e i fogli di stile o aggiungere nuove funzionalità]]> + + Connessione al database non riuscita. + Umbraco Versione 3 + Umbraco Versione 4 + Guarda + + Umbraco %0% per una nuova installazione o per l'aggiornamento dalla versione 3.0. +

+ Clicca "avanti" per avviare la procedura.]]> +
+ + + Codice cultura + Nome cultura + + + + Riconnetti adesso per salvare il tuo lavoro + + + Benvenuto + Benvenuto + Benvenuto + Benvenuto + Benvenuto + Benvenuto + Benvenuto + Fai il login qui sotto + Fai il login con + Sessione scaduta + + © 2001 - %0%
umbraco.com

]]> +
+ Password dimenticata? + + + + + + + Mostra password + Nascondi password + Ritorna alla finestra di login + Inserisci una nuova password + + + Umbraco: Reset Password + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Reset della password richiesto +

+

+ Il tuo username per effettuare l'accesso al backoffice di Umbraco è: %0% +

+

+ + + + + + +
+ + Clicca questo link per resettare la password + +
+

+

Se non riesci a cliccare sul link, copia e incolla questo URL nella finestra del browser:

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]> +
+ + + Dashboard + Sezioni + Contenuto + + + Scegli la pagina sopra... + + Seleziona dove il documento %0% deve essere copiato + + Seleziona dove il documento %0% deve essere spostato + + + + + + + + + + + + + + + + + + %0%]]> + Impostazioni di notifica salvate per + + + + Sono state modificate le lingue seguenti %0% + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Ciao %0%, +

+

+ Questa è un'email automatica per informare che l'azione '%1%' è stata eseguita sulla pagina '%2%' dall'utente '%3%' +

+ + + + + + +
+ +
+ MODIFICA
+
+

+

Riepilogo dell'aggiornamento:

+ %6% +

+

+ Buona giornata!

+ Saluti dal robot di Umbraco +

+
+
+


+
+
+ + + ]]> +
+ + Sono state modificate le seguenti lingue:

+ %0% + ]]> +
+ [%0%] Notifica per %1% eseguito su %2% + Notifiche + + + Azioni + Creati + Crea pacchetto + + + e selezionando il pacchetto. I pacchetti Umbraco generalmente hanno l'estensione ".umb" o ".zip". + ]]> + + + Trascina qui caricare + Includi tutti i figli + o clicca qui per scegliere il file del pacchetto + Carica pacchetto + + Installa un pacchetto locale dalla tua macchina. Installa solamente pacchetti + di cui ti fidi e ne conosci la provenienza + + Carica un altro pacchetto + Cancella e carica un altro pacchetto + Accetto + termini di servizio + Percorso del file + Percorso assoluto del file (es: /bin/umbraco.bin) + Installati + Pacchetti installati + Installa localmente + Termina + Questo pacchetto non ha una vista di configurazione + Non sono ancora stati creati pacchetti + Non hai installato nessun pacchetto + + 'Pacchetti' in alto a destra del tuo schermo]]> + + Azioni del pacchetto + + Contenuto del pacchetto + Files del pacchetto + + Installa pacchetto + Licenza + URL della licenza + + Cerca un pacchetto... + Risultati per + Non abbiamo trovato niente per + + Per favore prova a cercare un altro pacchetto oppure cerca tra le + categorie + + Popolari + Nuove uscite + ha + punti karma + Informazioni + Proprietario + Contributori + Creato + Versione corrente + Versione .NET + Downloads + Likes + + + + + Sorgenti esterne + Autore + Documentazione + Meta dati del pacchetto + Nome del pacchetto + Il pacchetto non contiene nessun elemento + +
+ È possibile rimuovere questo pacchetto dal sistema cliccando "rimuovi pacchetto" in basso.]]> +
+ Opzioni del pacchetto + Pacchetto leggimi + Repository del pacchetto + Conferma eliminazione + + + Disinstalla pacchetto + + + Avviso: tutti i documenti, i media, etc a seconda degli elementi che rimuoverai, smetteranno di funzionare, e potrebbero portare a un'instabilità del sistema, + perciò disinstalla con cautela. In caso di dubbio contattare l'autore del pacchetto.]]> + + Versione del pacchetto + Aggiornamento dalla versione + + + + + Sto disinstallando... + Sto scaricando... + Sto importando... + Sto installando... + Sto riavviando, per favore aspetta... + + + Per favore clicca 'Completa' per completare l'installazione e ricaricare la + pagina. + + Sto effettuando l'upload del pacchetto... + Verificato il funzionamento su Umbraco Cloud + + + + + + + + + + + + + + Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui + gruppi + + + + %0%]]> + %0% è ora protetta]]> + %0%]]> + + + + + %0%?]]> + + + + + %0%]]> + %0%]]> + Protezione specifica per membri + Se vuoi controllare gli accessi per membri specifici + + + Permessi insufficienti per pubblicare tutti i discendenti + + + + + + + + + + + + + + + + + + + + + + + + + Pubblicazione in corso, aspetta... + %0% di %1% pagine sono state pubblicate... + + %0% e le relative sottopagine sono state pubblicate + Pubblica %0% e tutte le sue sottopagine + + Pubblica per pubblicare %0% e quindi rendere visibile pubblicamente il suo contenuto.

+ Puoi pubblicare questa pagina e tutte le sue sottopagine spuntando Includi le sottopagine non pubblicate sotto. + ]]> +
+ + + Non hai configurato nessun colore + + + Puoi selezionare solo oggetti di tipo: %0% + Hai selezionato un elemento eliminato o nel cestino + + + + Elemento eliminato + Hai selezionato un media eliminato o nel cestino + + Eliminato + Apri nella libreria dei media + Cambia media + Resetta i tagli del media + Modifica %0% su %1% + Scartare la creazione? + + + Hai fatto delle modifiche a questo contenuto. Sei sicuro di volerle + scartare? + + Rimuovere? + Rimuovere tutti i media? + Appunti + Non permesso + + + + + + Link + + inserisci la didascalia da visualizzare + Inserisci il link + + + Resetta tagio + Salva taglio + Aggiungi nuovo taglio + Fatto + Annulla modifiche + Definite dall'utente + + + Modifiche + Seleziona una versione da confrontare con la versione corrente + + Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> + + + + + + + + + + + + + + Contenuto + Forms + Media + Membri + Pacchetti + Impostazioni + Traduzione + Utenti + + + Tours + I migliori video tutorial su Umbraco + Visita our.umbraco.com + Visita umbraco.tv + + + + + + + + + Tipo + Foglio di stile + Script + Tab + Titolo tab + Tabs + Tipo di contenuto Master abilitato + Questo tipo di contenuto usa + + + + Crea un template corrispondente + Aggiungi icona + + + Ordinamento + Data creazione + + + + + + + + Questo elemento non ha elementi figlio da ordinare + + + Validazione + + Gli errori di validazione devono essere sistemati prima che l'elemento possa + essere salvato + + Fallita + Salvato + Salvato. Per vedere le modifiche appena salvate ricarica la pagina + + + + Cancellato + + + + Tipo di dato: %1%]]> + + Tipo di documento salvato + Tab creata + Tab eliminata + Tab con id: %0% eliminata + + + + Tipo di dato salvato + + + + %0% documenti pubblicati e visibili sul sito web + %0% pubblicati e visibili sul sito web + %0% documenti pubblicati per le lingue %1% e visibili sul sito web + Contenuto salvato + + + + + %0% salvata + + Le modifiche sono state inviate per l'approvazione + %0% modifiche sono state inviate per l'approvazione + Media salvato + Media salvato senza nessun errore + Membro salvato + Gruppo di Membri salvato + + + + + + Tipo di utente salvato + Gruppo di utenti salvato + Hostnames salvati + Errore durante il salvataggio degli hostnames + + + + + + + + Tipo di Media salvato + Tipo di Membro salvato + Gruppo di Membri salvato + Template non salvato + Per favore controlla di non avere due templates con lo stesso alias + Template salvato + Template salvato con successo! + Contenuto non pubblicato + Variazione di contenuto %0% non pubblicata + + + + Partial view salvata + Partial view salvata senza errori! + Partial view non salvata + Errore durante il salvataggio del file. + Permessi salvati per + Eliminati %0% gruppi di utenti + + Abilitati %0% utenti + Disabilitati %0% utenti + + + I gruppi di utenti sono stati assegnati + Sono stati sbloccati %0% utenti + + + + + Invita utenti + + + + + Validazione fallita per la lingua '%0%' + + + + + + + + + + + + + + + + + + Aggiungi stile + Modifica stile + Stili del Rich text editor + + Definisci gli stili che dovranno essere disponibili nel Rich text editor per questo + foglio di stile + + + + + Anteprima + + Selettore + + Stili + + Codice + Rich Text Editor + + + Impossibile eliminare il template con ID %0% + Modifica template + Sezioni + Inserisci area di contenuto + Inserisci segnaposto per l'area di contenuto + Inserisci + Scegli cosa inserire nel tuo template + Elementi del dizionario + + + + Macro + + + + Valore + + Visualizza il valore di un campo nominato dalla pagina corrente, con delle opzioni + per modificare il valore o impostare il valore di fallback. + + Partial view + + + + Master template + No master + Renderizza template figli + + @RenderBody(). + ]]> + + Definisci una sezione nominata + + @section { ... }. Questa potrà poi essere renderizzata in una + specifica area del padre di questo template, usando @RenderSection. + ]]> + + Renderizza una sezione nominata + + @RenderSection(name). + Questo renderizzerà un'area di un template figlio avvolta nel corrispondente @section [name]{ ... } definition. + ]]> + + Nome della sezione + + + @section, altrimenti verrà visualizzato un errore. + ]]> + + Generatore di query + oggetti trovati, in + copia negli appunti + Voglio + tutti i contenuti + contenuti di tipo "%0%" + da + il mio sito web + dove + e + + + prima + prima (comprese le date selezionate) + dopo + dopo (comprese le date selezionate) + + + contiene + non contiene + maggiore di + maggiore di o uguale a + minore di + minore di o uguale a + Id + Nome + Data di creazione + Ultima data di aggiornamento + ordina per + ascendente + discendente + Template + + + Immagine + Macro + Seleziona il tipo di contenuto + Seleziona un layout + Aggiungi una riga + Aggiungi contenuto + Elimina contenuto + Impostazioni applicate + + + Clicca per incorporare + Clicca per inserire un immagine + Clicca per inserire una macro + Didascalia dell'immagine... + Scrivi qui... + I Grid Layout + + I layout sono l'area globale di lavoro per il grid editor, di solito ti servono + solamente uno o due layout differenti + + Aggiungi un Grid Layout + Modifica il Grid Layout + + Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori + sezioni + + Configurazioni della riga + Le righe sono le colonne predefinite disposte orizzontalmente + Aggiungi configurazione della riga + Modifica configurazione della riga + + Sistema la riga impostando la larghezza della colonna ed aggiungendo + ulteriori colonne + + Nessuna ulteriore configurazione disponibile + Colonne + Totale combinazioni delle colonne nel grid layout + Impostazioni + Configura le impostazioni che possono essere cambiate dai editori + Stili + Configura i stili che possono essere cambiati dai editori + Permetti tutti i editor + Permetti tutte le configurazioni della riga + Oggetti massimi + Lascia vuoto o imposta a 0 per illimitati + Imposta come predefinito + Scegli aggiuntivi + Scegli predefinito + sono stati aggiunti + Attenzione + Stai eliminando le configurazioni della riga + + Eliminando un nome della configurazione della riga perderai i dati associati a qualsiasi contenuto basato su + questa configurazione. + + + + Composizioni + Gruppi + Non hai aggiunto nessun gruppo + Aggiungi gruppo + Ereditato da + + Etichetta richiesta + Abilita la vista lista + + Configura l'oggetto per visualizzare una lista dei suoi figli, ordinabili e + ricercabili. I figli non saranno visualizzati nell'albero dei contenuti + + Templates abilitati + + Scegli che templates sono abilitati all'utilizzo per i contenuti di questo + tipo. + + Consenti come nodo root + + Consenti agli editori la creazione di questo tipo di contenuto a livello root. + + Tipi di nodi figlio consentiti + + Consenti la creazione di contenuti con i tipi specificati sotto il contenuto di + questo tipo. + + Scegli nodo figlio + + + + + + + Non ci sono tipi di contenuto utilizzabili come composizione. + + + + Crea nuovo + Usa esistente + Impostazioni dell'editor + Configurazioni disponibili + Crea una nuova configurazione + Configurazione + Si, elimina + + + Seleziona la cartella da spostare + Seleziona la cartella da copiare + nella struttura sottostante + Tutti i tipo di documento + Tutti i documenti + Tutti i media + + che usano questo tipo di documento verranno eliminati permanentemente, sei sicuro di + voler eliminare anche questi? + + + che usano questo tipo di media verranno eliminati permanentemente, sei sicuro di voler + eliminare anche questi? + + + che usano questo tipo di membro verranno eliminati permanentemente, sei sicuro di voler + eliminare anche questi? + + e tutti i documenti che usano questo tipo + e tutti i media che usano questo tipo + e tutti i membri che usano questo tipo + + + Abilita il membro alla modifica di questo valore dalla pagina del suo + profilo. + + Dati sensibili + + + + Visualizza sul profilo del membro + + Permette a questo valore di essere visualizzato sulla pagina del profilo + del membro + + la scheda non ha un ordine + + + + + Consenti variazioni + Consenti variazioni in base alla lingua + Consenti segmentazione + Varia in base alla cultura + Varia per segmenti + + Consenti agli editors la creazione di contenuto di questo tipo in lingue + differenti. + + Consenti agli editors la creazione di contenuto in diverse lingue. + Consenti agli editors la creazione di segmenti per questo contenuto. + Consenti variazioni in base alla lingua + Consenti segmentazione + Tipo di elemento + + + + + + + + + + + + Aspetto + Etichetta sopra (larghezza intera) + + + Aggiungi lingua + Lingua obbligatoria + + + + Lingua di default + + + Il cambio della lingua predefinita potrebbe comportare la mancanza di + contenuti predefiniti. + + Ripiega a + Nessuna lingua alternativa + + + + Lingua alternativa + nessuna + + + Aggiungi parametro + Modifica parametro + Inserisci il nome della macro + Parametri + + Definisci i parametri che dovranno essere disponibili utilizzando questa macro. + + Seleziona il file partial view per la macro + + + Compilazione modelli in corso... + + Modelli generati + Impossibile generare i modelli + + La generazione dei modelli non è andata a buon fine, consulta i log di Umbraco + + + + Aggiungi campo alternativo + Campo alternativo + Aggiungi valore predefinito + Valore predefinito + Campo alternativo + Valore predefinito + + Codifica + Scegli il campo + Converte le interruzioni di linea + Si, converti le interruzioni di linea + + Campi Personalizzati + Solo data + Formato e codifica + + Formatta il valore in data, o in data e ora, secondo la cultura corrente + + + + + Minuscolo + Modifica output + Nessuno + Esempio di output + + + Ricorsivo + Si, rendilo ricorsivo + Separatore + Campi Standard + Maiuscolo + + + + + + + Data e ora + + + Dettagli di traduzione + Scarica XML DTD + Campi + Includi le sottopagine + + + + + + + + + + + + + + + + + Traduttore + + + + Contenuti + Modelli di contenuto + Media + Cache Browser + Cestino + Pacchetti creati + Tipi di dato + Dizionario + Pacchetti installati + Installare skin + Installare starter kit + Lingue + Installa un pacchetto locale + Macros + Tipi di media + Membri + Gruppi di Membri + Ruoli + Tipi di membro + Tipi di documento + Tipi di relazione + Pacchetti + Pacchetti + Partial Views + Macro Partial Views + Installa dal repository + Installa Runway + Moduli Runway + Files di scripting + Scripts + Fogli di stile + Templates + Logs + Utenti + Impostazioni + Templating + Terze parti + + + + + + + + + + + Accesso + + Basandosi sui gruppi assegnati e sui nodi di partenza, l'utente ha accesso ai seguenti + nodi + + Assegna accessi + Amministratore + Campo Categoria + Utente creato il + Cambia la tua password + Cambia foto + Nuova password + Minimo %0% caratteri! + Dovrebbero esserci almeno %0% caratteri speciali qui. + + + Conferma la nuova password + + + + Contenuto del canale + Crea un altro utente + + Crea nuovi utenti per dar loro accesso a Umbraco. Quando un nuovo utente viene creato, + una nuova password da condividere con l'utente viene generata. + + Campo Descrizione + Disabilita l'utente + Tipo di Documento + Editor + Campo Eccezione + Tentativi di accesso falliti + Vai al profilo dell'utente + Crea gruppi per assegnare gli accessi e i permessi + Invita un altro utente + + + + Lingua + Imposta la lingua che vedrai nei menu e nei dialoghi + Data dell'ultimo blocco + Ultimo login + Ultima modifica della password + Login + Nodo di inizio nella sezione Media + Limita la libreria multimediale a un nodo iniziale specifico + Nodi di inizio nella sezione Media + + + + Sezioni + + non ha ancora effettuato l'accesso + Vecchia password + Password + Resetta password + + Password cambiata + + + + Password attuale + + + + + + + + + Rimuovi foto + Permessi predefiniti + Permessi granulari + Imposta i permessi per nodi specifici + Profilo + + Aggiungi sezioni per cui l'utente deve avere accesso + Seleziona gruppi di utenti + Nodo di partenza non selezionato + Nodi di partenza non selezionati + + Limita l'albero dei contenuti a un nodo iniziale specifico + Nodi di inizio del contenuto + + Ultimo aggiornamento utente + + + + + Gestione utenti + Username + + Gruppo di utenti + + + + + + Ciao e benvenuto su Umbraco! In solo 1 minuto sarai pronto a partire, dovrai + solamente impostare una password. + + + + + Autore + Modifica + Il tuo profilo + + La sessione scade in + Invita utente + Crea utente + Invia invito + Torna agli utenti + Invito per Umbraco + + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Ciao %0%, +

+

+ Sei stato invitato nel Back Office di Umbraco da %1%. +

+

+ Messaggio da %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Clicca questo link per accettare l'invito + +
+
+

Se non puoi cliccare sul link, copia e incolla questo URL in una nuova finestra del tuo browser:

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + ]]> +
+ Invita + Sto rinviando l'invito... + Cancella utente + Sei sicuro di voler cancellare questo account utente? + Tutti + Attivi + Disabilitati + Bloccati + Invitati + Inattivi + Nome (A-Z) + Nome (Z-A) + + + Ultimo login + Non sono stati aggiunti gruppi di utenti + + + Validazione + Valida come indirizzo email + Valida come numero + Valida come URL + ...oppure inserisci una validazione personalizzata + + Inserisci un errore di validazione personalizzato (opzionale) + Inserisci una regular expression + Inserisci un errore di validazione personalizzato (opzionale) + Devi aggiungere almeno + Puoi avere solamente + Aggiungi fino a + elementi + URL + URL selezionati + elementi selezionati + Data non valida + + + Email non valida + + + + Validazione personalizzata + %1% in più.]]> + %1% di troppo.]]> + + + + + + + + + + + + + + + Gli errori personalizzati sono impostati a '%0%'. + + + + Errori personalizzati impostati correttamente a '%0%'. + I MacroErrors sono impostati a '%0%'. + + + + I MacroErrors sono ora impostati a '%0%'. + + + + + + + + + + + + Il file seguente non esiste: '%0%'. + + '%0%' nel file di configurazione '%1%'.]]> + + + + + + + + + Sono stati trovati %0% problemi con lo schema del database + (Controlla i log per dettagli aggiuntivi) + + + Sono stati trovati alcuni errori durante la validazione + dello schema del database per la versione corrente di Umbraco. + + + Errore di validazione del certificato SSL: '%0%' + + + + + Errore durante il ping dell'URL %0% - '%1%' + + Attualmente %0% stai visualizzando il sito web utilizzando lo schema + HTTPS. + + + + + + + + + Non posso aggiornare l'impostazione 'Umbraco.Core.UseHttps' nel file + web.config. Errore: %0% + + + Abilita HTTPS + + Imposta il valore di umbracoSSL a true nelle appSetting del file + web.config. + + + + + Correggi + + Non posso correggere un controllo con un valore di confronto pari a + 'ShouldNotEqual'. + + + Non posso correggere un controllo con un valore di confronto pari a + 'ShouldEqual' con un valore fornito. + + + + + + + + + + + + + + + + + Tutte le cartelle hanno i permessi corretti impostati. + + + %0%.]]> + + + %0%. Se non vengono scritte non è necessario intraprendere alcuna azione.]]> + + Tutti i file hanno i permessi corretti impostati. + + + %0%.]]> + + + %0%. Se non vengono scritti non è necessario intraprendere alcuna azione.]]> + + + X-Frame-Options usato per controllare se un sito può essere inserito in un IFRAME da un altro è stato trovato.]]> + + + X-Frame-Options usato per controllare se un sito può essere inserito in un IFRAME da un altro non è stato trovato.]]> + + Imposta l'header nella configurazione + + Aggiunge un valore alla sezione httpProtocol/customHeaders del + file web.config per prevenire che un sito possa essere inserito in un IFRAME da altri siti web. + + + + + Non posso aggiornare il file web.config. Errore: %0% + + X-Content-Type-Options usato per proteggere dalle vulnerabilità per MIME sniffing è stato trovato.]]> + + + X-Content-Type-Options usato per proteggere dalle vulnerabilità per MIME sniffing è stato trovato.]]> + + + + + + + + + Strict-Transport-Security, conosciuto anche come l'header HSTS, è stato trovato.]]> + + + Strict-Transport-Security non è stato trovato.]]> + + + Aggiunge l'header 'Strict-Transport-Security' con il valore + 'max-age=10886400' alla sezione httpProtocol/customHeaders del file web.config. Usa questa correzione solo se + avrai i tuoi domini in esecuzione con https per le prossime 18 settimane (minimo). + + + + X-XSS-Protection è stato trovato.]]> + + + X-XSS-Protection non è stato trovato.]]> + + + Aggiunge l'header 'X-XSS-Protection' con il valore '1; + mode=block' alla sezione httpProtocol/customHeaders del file web.config. + + + + + + + %0%.]]> + + + Non sono stati trovati header che rivelano informazioni riguardo alla + tecnologia utilizzata per il sito. + + + + + + + + + Le impostazioni SMTP sono state inserite correttamente e il servizio + funziona come previsto. + + + + + + %0%.]]> + + + %0%.]]> + + +

I risultati dell'Health Check programmato di Umbraco del %0% alle %1% è il seguente:

%2%]]> +
+ Stato dell'Health Check di Umbraco: %0% + Controlla tutti i gruppi + Controlla gruppo + + L'health checker valuta varie aree del tuo sito per le impostazioni delle migliori pratiche, la configurazione, i potenziali problemi, ecc. Puoi facilmente risolvere i problemi premendo un pulsante. + Puoi aggiungere i tuoi health check personalizzati, guarda sulla documentazione per più informazioni riguardo i custom health checks.

+ ]]> +
+ + + Disabilita tracciamento degli URL + Abilita tracciamento degli URL + Cultura + URL originale + Reindirizzato a + Gestione Redirect URL + I seguenti URL reindirizzano a questo contenuto: + + + + + Sei sicuro di voler eliminare il reindirizzamento da '%0%' a '%1%'? + Reindirizzamento URL rimosso. + Errore durante la rimozione del reindirizzamento URL. + + Sei sicuro di voler disabilitare il tracciamento degli URL? + + + + + + + + + + + Nessun elemento del dizionario tra cui scegliere + + + %0% caratteri rimasti.]]> + %1% di troppo.]]> + + + + Contenuto cestinato con Id: {0} relativo al contenuto principale originale con Id: {1} + + Media cestinato con Id: {0} relativo al media principale originale con Id: {1} + Impossibile ripristinare automaticamente questo elemento + + Non esiste una posizione in cui questo elemento possa essere ripristinato + automaticamente. Puoi spostare l'elemento manualmente utilizzando l'albero sottostante. + + + + + Direzione + Da genitore a figlio + Bidirezionale + Genitore + Figlio + Numero + Relazioni + Creato + Commento + Nome + Nessuna relazione per questo tipo di relazione + Tipo di relazione + Relazioni + + + Guida introduttiva + Gestione Redirect URL + Contenuto + Benvenuto + Gestione Examine + Stato di pubblicazione + Models Builder + Health Check + Profilazione + Guida introduttiva + Installa Umbraco Forms + + + Indietro + Layout attivo: + Vai a + gruppo + passato + attenzone + fallito + suggerimento + Check passato + Check fallito + Apri la ricerca nel backoffice + Apri/chiudi l'aiuto del backoffice + Apri/chiudi le opzioni del tuo profilo + Imposta le Culture e gli Hostnames per %0% + Crea nuovo nodo sotto %0% + Imposta le restrizioni di accesso per %0% + Imposta i permessi per %0% + Modifica l'ordinamento per %0% + Crea un modello di contenuto basato su %0% + Apri il menu contestuale per + Lingua corrente + Cambia lingua in + Crea nuova cartella + Partial View + Partial View Macro + Membro + Tipo di dato + Cerca nella dashboard di reindirizzamento + Cerca nella sezione dei gruppi di utenti + Cerca tra gli utenti + Crea oggetto + Crea + Modifica + Nome + Aggiungi nuova riga + + Cerca nel backoffice di Umbraco + Cerca contenuti, media, ecc. nel backoffice. + + + + Percorso: + Trovato in + Ha una traduzione + Traduzione mancante + Voci del dizionario + Seleziona una delle opzioni per modificare il nodo. + Esegui l'azione %0% sul nodo %1% + Aggiungi una descrizione per l'immagine + Cerca nell'albero dei contenuti + Numero massimo + + + Riferimenti + Questo tipo di dato non ha riferimenti. + Usato nei tipi di documento + Non ci sono riferimenti a tipi di documento. + Usato nei tipi di media + Non ci sono riferimenti a tipi di media. + Usato nei tipi di membro + Non ci sono riferimenti a tipi di membro. + Usato da + Correlato ai seguenti elementi + Usato nei documenti + Usato nei membri + Usato nei media + + + Elimina ricerca salvata + Livelli di log + Seleziona tutto + Deselezionare tutto + Ricerche salvate + Salva ricerca + Inserisci un nome descrittivo per la tua query di ricerca + Filtra la ricerca + Risultati totali + Timestamp + Livello + Macchina + Messaggio + Exception + + Ricerca con Google + Ricerca questo messaggio con Google + Ricerca con Bing + Ricerca questo messaggio con Bing + Ricerca su Our Umbraco + + Ricerca questo messaggio sui forum e le documentazioni di + Our Umbraco + + Ricerca su Our Umbraco con Google + Ricerca sui forum di Our Umbraco con Google + Ricerca nel codice sorgente di Umbraco + Ricerca nel codice sorgente di Umbraco su GitHub + Ricerca tra i problemi di Umbraco + Ricerca tra i problemi di Umbraco su GitHub + Elimina questa ricerca + Trova log con Request ID + Trova log con Namespace + Trova log con Machine Name + Apri + Polling + Ogni 2 secondi + Ogni 5 secondi + Ogni 10 secondi + Ogni 20 secondi + Ogni 30 secondi + Polling ogni 2s + Polling ogni 5s + Polling ogni 10s + Polling ogni 20s + Polling ogni 30s + + + Copia %0% + %0% da %1% + Lista di %0% + Rimuovi tutti gli oggetti + Svuota appunti + + + + + + + Attendi + Aggiorna stato + Memory Cache + + + + Ricarica + Cache del Database + + La ricostruzione può metterci del tempo. + Usalo quando la ricarica della Memory Cache non è sufficiente e pensi che la cache del database non sia + stata generata correttamente, che indicherebbe un problema critico di Umbraco. + ]]> + + Ricostruisci + Interni + + non hai bisogno di usarlo. + ]]> + + Raccogli + Stato della Published Cache + Caches + + + Profilazione delle performance + + + Umbraco attualmente funziona in modalità debug. Ciò significa che puoi utilizzare il profiler delle prestazioni integrato per valutare le prestazioni durante il rendering delle pagine. +

+

+ Se vuoi attivare il profiler per il rendering di una pagina specifica, aggiungi semplicemente umbDebug=true alla querystring quando richiedi la pagina. +

+

+ Se vuoi che il profiler sia attivato per impostazione predefinita per tutti i rendering di pagina, puoi utilizzare l'interruttore qui sotto. + Verrà impostato un cookie nel tuo browser, che quindi attiverà automaticamente il profiler. + In altre parole, il profiler sarà attivo per impostazione predefinita solo nel tuo browser, non in quello di tutti gli altri. +

+ ]]> +
+ Attiva la profilazione per impostazione predefinita + Promemoria + + + Non dovresti mai lasciare che un sito di produzione venga eseguito in modalità debug. La modalità di debug viene disattivata impostando debug="false" nell'elemento <compilation /> nel file web.config. +

+ ]]> +
+ + + Umbraco attualmente non viene eseguito in modalità debug, quindi non è possibile utilizzare il profiler integrato. Questo è come dovrebbe essere per un sito produttivo. +

+

+ La modalità di debug viene attivata impostando debug="true" nell'elemento <compilation /> in web.config. +

+ ]]> +
+ + + Ore di videoallenamenti su Umbraco sono a solo un click da te + + Vuoi padroneggiare Umbraco? Dedica un paio di minuti all'apprendimento di alcune best practice guardando uno di questi video sull'utilizzo di Umbraco. Visita umbraco.tv per altri video su Umbraco

+ ]]> +
+ Per iniziare + + + Inizia da qui! + + + + + + nella documentazione di Our Umbraco + ]]> + + + Community Forum + ]]> + + + video tutorial (alcuni sono gratuiti, altri richiedono un abbonamento) + ]]> + + + strumenti per aumentare la produttività e il supporto commerciale + ]]> + + + certificazione + ]]> + + + + Benvenuto nell'amichevole CMS + + + + + + Umbraco Forms + + Crea moduli utilizzando un'interfaccia drag and drop intuitiva. Da semplici moduli di + contatto che inviano e-mail a questionari avanzati che si integrano con i sistemi CRM. I tuoi clienti lo + adoreranno! + + + + Scegli tipo di elemento + Allega un tipo di elemento delle impostazioni + Seleziona vista + Seleziona foglio di stile + Scegli miniatura + Crea nuovo tipo di elemento + Foglio di stile personalizzato + Aggiungi foglio di stile + Aspetto dell'editor + Modelli di dati + Aspetto del catalogo + Colore di sfondo + Colore dell'icona + Modello del contenuto + Etichetta + Vista personalizzata + Mostra la descrizione della vista personalizzata + + + + Modello di impostazioni + Dimensione dell'editor di sovrapposizione + Aggiungi vista personalizzata + Aggiungi impostazioni + Sovrascrivi modello di etichetta + + %0%?]]> + + + %0%?]]> + + + + + + Miniatura + Aggiungi miniatura + Crea vuota + Appunti + Impostazioni + Avanzate + Forza nascondi editor di contenuti + Hai fatto delle modifiche a questo contenuto. Sei sicuro di volerle scartare? + Scartare la creazione? + + Errore! + + + + Aggiungi contenuto + Aggiungi %0% + + + + + + Cosa sono i modelli di contenuto? + + I modelli di contenuto sono contenuti predefiniti che possono essere selezionati + durante la creazione di un nuovo nodo di contenuto. + + Come creo un modello di contenuto? + + Ci sono due modi per creare un modello di contenuto:

+
    +
  • Fare clic con il pulsante destro del mouse su un nodo di contenuto e selezionare "Crea modello di contenuto" per creare un nuovo modello di contenuto.
  • +
  • Fare clic con il pulsante destro del mouse sull'albero dei modelli di contenuto nella sezione Impostazioni e selezionare il tipo di documento per il quale si desidera creare un modello di contenuto.
  • +
+

Una volta specificato un nome, gli editors potranno cominciare a usare il tipo di contenuto come base per la nuova pagina.

+ ]]> +
+ Come gestisco i modelli di contenuto? + + Puoi modificare ed eliminare i modelli di contenuto dall'albero "Modelli di + contenuto" nella sezione Impostazioni. Espandere il tipo di documento su cui si basa il modello di contenuto e + fare clic su di esso per modificarlo o eliminarlo. + + + + Chiudi + Chiudi anteprima + Anteprima sito web + + Aprire l'anteprima del sito web? + + + + Anteprima dell'ultima versione + Visualizza versione pubblicata + Vuoi vedere la versione pubblicata? + + + + Visualizza versione pubblicata + + + + oggetto trovato + oggetti trovati + +
diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index 73157767b2..0a0daeda5a 100644 --- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs @@ -384,7 +384,10 @@ public static class ClaimsIdentityExtensions var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier); if (firstValue is not null) { - return int.Parse(firstValue, CultureInfo.InvariantCulture); + if (int.TryParse(firstValue, CultureInfo.InvariantCulture, out var id)) + { + return id; + } } return null; diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 233a67fa62..33a4566ce5 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -237,7 +237,7 @@ public static class ContentRepositoryExtensions { foreach (ContentCultureInfos cultureInfo in other.CultureInfos) { - if (culture == "*" || culture == cultureInfo.Culture) + if (culture == "*" || culture.InvariantEquals(cultureInfo.Culture)) { content.SetCultureName(cultureInfo.Name, cultureInfo.Culture); } diff --git a/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs b/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs index df5aab16c7..61659bd7dc 100644 --- a/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentTreeChangeNotification.cs @@ -32,4 +32,14 @@ public class ContentTreeChangeNotification : TreeChangeNotification : base(new TreeChange(target, changeTypes), messages) { } + + public ContentTreeChangeNotification( + IContent target, + TreeChangeTypes changeTypes, + IEnumerable? publishedCultures, + IEnumerable? unpublishedCultures, + EventMessages messages) + : base(new TreeChange(target, changeTypes, publishedCultures, unpublishedCultures), messages) + { + } } diff --git a/src/Umbraco.Core/PropertyEditors/RadioValueEditor.cs b/src/Umbraco.Core/PropertyEditors/RadioValueEditor.cs new file mode 100644 index 0000000000..10a80e062b --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/RadioValueEditor.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Core.PropertyEditors; + +public class RadioValueEditor : DataValueEditor +{ + public RadioValueEditor( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) => + Validators.Add(new RadioValueValidator()); +} diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs index 3dc4ea2057..68b90c7403 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace Umbraco.Cms.Core.PropertyEditors; /// @@ -15,10 +13,4 @@ public class SliderConfiguration [ConfigurationField("maxVal")] public decimal MaximumValue { get; set; } - - [ConfigurationField("initVal1")] - public decimal InitialValue1 { get; set; } - - [ConfigurationField("initVal2")] - public decimal InitialValue2 { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs deleted file mode 100644 index 589075b936..0000000000 --- a/src/Umbraco.Core/PropertyEditors/TrueFalseConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Umbraco.Cms.Core.PropertyEditors; - -/// -/// Represents the configuration for the true/false (toggle) value editor. -/// -public class TrueFalseConfiguration -{ - [ConfigurationField("default")] - public bool InitialState { get; set; } -} diff --git a/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs deleted file mode 100644 index 0e9bfd0d36..0000000000 --- a/src/Umbraco.Core/PropertyEditors/TrueFalseConfigurationEditor.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Umbraco.Cms.Core.IO; - -namespace Umbraco.Cms.Core.PropertyEditors; - -/// -/// Represents the configuration editor for the true/false (toggle) value editor. -/// -public class TrueFalseConfigurationEditor : ConfigurationEditor -{ - public TrueFalseConfigurationEditor(IIOHelper ioHelper) - : base(ioHelper) - { - } -} diff --git a/src/Umbraco.Core/PropertyEditors/Validators/MultipleValueValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/MultipleValueValidator.cs new file mode 100644 index 0000000000..193937b9f6 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/Validators/MultipleValueValidator.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +public class MultipleValueValidator : IValueValidator +{ + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) + { + // don't validate if empty + if (value == null || value.ToString().IsNullOrWhiteSpace()) + { + yield break; + } + + if (dataTypeConfiguration is not ValueListConfiguration valueListConfiguration) + { + yield break; + } + + if (value is not IEnumerable values) + { + yield break; + } + + foreach (var selectedValue in values) + { + if (valueListConfiguration.Items.Contains(selectedValue) is false) + { + yield return new ValidationResult( + $"The value {selectedValue} is not a part of the pre-values", ["items"]); + } + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RadioValueValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RadioValueValidator.cs new file mode 100644 index 0000000000..742360e3c9 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/Validators/RadioValueValidator.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.Validators; + +public class RadioValueValidator : IValueValidator +{ + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, + PropertyValidationContext validationContext) + { + // don't validate if empty + if (value == null || value.ToString().IsNullOrWhiteSpace()) + { + yield break; + } + + if (dataTypeConfiguration is not ValueListConfiguration valueListConfiguration) + { + yield break; + } + + if (value is not string valueAsString) + { + yield break; + } + + if (valueListConfiguration.Items.Contains(valueAsString) is false) + { + yield return new ValidationResult( + $"The value {valueAsString} is not a part of the pre-values", ["items"]); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index ba3a1e552b..d499efcd4f 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -37,6 +37,12 @@ public class DecimalValueConverter : PropertyValueConverterBase return Convert.ToDecimal(sourceDouble); } + // is it an integer? + if (source is int sourceInteger) + { + return Convert.ToDecimal(sourceInteger); + } + // is it a string? if (source is string sourceString) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs index 0a34668e23..28eedeb797 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -49,29 +49,16 @@ public class SliderValueConverter : PropertyValueConverterBase /// public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) { - SliderConfiguration? configuration = propertyType.DataType.ConfigurationAs(); - bool isRange = IsRange(configuration); + bool isRange = IsRange(propertyType); var sourceString = source?.ToString(); - // If source is null, the returned value depends on the configured initial values. - if (string.IsNullOrEmpty(sourceString)) - { - return isRange - ? new Range - { - Minimum = configuration?.InitialValue1 ?? 0M, - Maximum = configuration?.InitialValue2 ?? 0M - } - : configuration?.InitialValue1 ?? 0M; - } - return isRange ? HandleRange(sourceString) : HandleDecimal(sourceString); } - private static Range HandleRange(string sourceString) + private static Range HandleRange(string? sourceString) { if (sourceString is null) { @@ -105,8 +92,13 @@ public class SliderValueConverter : PropertyValueConverterBase return new Range(); } - private static decimal HandleDecimal(string sourceString) + private static decimal HandleDecimal(string? sourceString) { + if (string.IsNullOrEmpty(sourceString)) + { + return default; + } + // This used to be a range slider, so we'll assign the minimum value as the new value if (sourceString.Contains(',')) { @@ -131,9 +123,7 @@ public class SliderValueConverter : PropertyValueConverterBase private static bool TryParseDecimal(string? representation, out decimal value) => decimal.TryParse(representation, NumberStyles.Number, CultureInfo.InvariantCulture, out value); - private static bool IsRange(IPublishedPropertyType propertyType) - => IsRange(propertyType.DataType.ConfigurationAs()); - private static bool IsRange(SliderConfiguration? configuration) - => configuration?.EnableRange == true; + private static bool IsRange(IPublishedPropertyType propertyType) + => propertyType.DataType.ConfigurationAs()?.EnableRange == true; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs index 96b8afc250..f3953d73a3 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -18,7 +18,7 @@ public class YesNoValueConverter : PropertyValueConverterBase { if (source is null) { - return null; + return false; } // in xml a boolean is: string @@ -59,17 +59,4 @@ public class YesNoValueConverter : PropertyValueConverterBase // false for any other value return false; } - - /// - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) - { - // If source is null, whether we return true or false depends on the configured default value (initial state). - if (source is null) - { - TrueFalseConfiguration? configuration = propertyType.DataType.ConfigurationAs(); - return configuration?.InitialState ?? false; - } - - return (bool)source; - } } diff --git a/src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs b/src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs new file mode 100644 index 0000000000..aa3d322381 --- /dev/null +++ b/src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Routing; + +public interface IPublishedUrlInfoProvider +{ + /// + /// Gets all published urls for a content item. + /// + /// The content to get urls for. + /// Set of all published url infos. + Task> GetAllAsync(IContent content); +} diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs index 3818bc2776..bbc0f4011b 100644 --- a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs @@ -117,7 +117,7 @@ public class NewDefaultUrlProvider : IUrlProvider // although we are passing in culture here, if any node in this path is invariant, it ignores the culture anyways so this is ok var route = GetLegacyRouteFormatById(key, culture); - if (route == null) + if (route == null || route == "#") { continue; } diff --git a/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs b/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs new file mode 100644 index 0000000000..d4059bcab8 --- /dev/null +++ b/src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs @@ -0,0 +1,122 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Routing; + +public class PublishedUrlInfoProvider : IPublishedUrlInfoProvider +{ + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly ILanguageService _languageService; + private readonly IPublishedRouter _publishedRouter; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILocalizedTextService _localizedTextService; + private readonly ILogger _logger; + private readonly UriUtility _uriUtility; + private readonly IVariationContextAccessor _variationContextAccessor; + + public PublishedUrlInfoProvider( + IPublishedUrlProvider publishedUrlProvider, + ILanguageService languageService, + IPublishedRouter publishedRouter, + IUmbracoContextAccessor umbracoContextAccessor, + ILocalizedTextService localizedTextService, + ILogger logger, + UriUtility uriUtility, + IVariationContextAccessor variationContextAccessor) + { + _publishedUrlProvider = publishedUrlProvider; + _languageService = languageService; + _publishedRouter = publishedRouter; + _umbracoContextAccessor = umbracoContextAccessor; + _localizedTextService = localizedTextService; + _logger = logger; + _uriUtility = uriUtility; + _variationContextAccessor = variationContextAccessor; + } + + /// + public async Task> GetAllAsync(IContent content) + { + HashSet urlInfos = []; + var cultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray(); + + // First we get the urls of all cultures, using the published router, meaning we respect any extensions. + foreach (var culture in cultures) + { + var url = _publishedUrlProvider.GetUrl(content.Key, culture: culture); + + // Handle "could not get URL" + if (url is "#" or "#ex") + { + urlInfos.Add(UrlInfo.Message(_localizedTextService.Localize("content", "getUrlException"), culture)); + continue; + } + + // Check for collision + Attempt hasCollision = await VerifyCollisionAsync(content, url, culture); + + if (hasCollision is { Success: true, Result: not null }) + { + urlInfos.Add(hasCollision.Result); + continue; + } + + urlInfos.Add(UrlInfo.Url(url, culture)); + } + + // Then get "other" urls - I.E. Not what you'd get with GetUrl(), this includes all the urls registered using domains. + // for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them. + foreach (UrlInfo otherUrl in _publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture)) + { + urlInfos.Add(otherUrl); + } + + return urlInfos; + } + + private async Task> VerifyCollisionAsync(IContent content, string url, string culture) + { + var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri is false) + { + uri = uri.MakeAbsolute(_umbracoContextAccessor.GetRequiredUmbracoContext().CleanedUmbracoUrl); + } + + uri = _uriUtility.UriToUmbraco(uri); + IPublishedRequestBuilder builder = await _publishedRouter.CreateRequestAsync(uri); + IPublishedRequest publishedRequest = await _publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); + + if (publishedRequest.HasPublishedContent() is false) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + const string logMsg = nameof(VerifyCollisionAsync) + + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; + _logger.LogDebug(logMsg, url, uri, culture); + } + + var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeErrorCannotRoute"), culture); + return Attempt.Succeed(urlInfo); + } + + if (publishedRequest.IgnorePublishedContentCollisions) + { + return Attempt.Fail(); + } + + if (publishedRequest.PublishedContent?.Id != content.Id) + { + var collidingContent = publishedRequest.PublishedContent?.Key.ToString(); + + var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeError", [collidingContent]), culture); + return Attempt.Succeed(urlInfo); + } + + // No collision + return Attempt.Fail(); + } +} diff --git a/src/Umbraco.Core/Services/Changes/TreeChange.cs b/src/Umbraco.Core/Services/Changes/TreeChange.cs index bb722dce24..70adf1e005 100644 --- a/src/Umbraco.Core/Services/Changes/TreeChange.cs +++ b/src/Umbraco.Core/Services/Changes/TreeChange.cs @@ -8,10 +8,22 @@ public class TreeChange ChangeTypes = changeTypes; } + public TreeChange(TItem changedItem, TreeChangeTypes changeTypes, IEnumerable? publishedCultures, IEnumerable? unpublishedCultures) + { + Item = changedItem; + ChangeTypes = changeTypes; + PublishedCultures = publishedCultures; + UnpublishedCultures = unpublishedCultures; + } + public TItem Item { get; } public TreeChangeTypes ChangeTypes { get; } + public IEnumerable? PublishedCultures { get; } + + public IEnumerable? UnpublishedCultures { get; } + public EventArgs ToEventArgs() => new EventArgs(this); public class EventArgs : System.EventArgs diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 8ffb6e4094..ce1a724e44 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1642,7 +1642,12 @@ public class ContentService : RepositoryService, IContentService // events and audit scope.Notifications.Publish( new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState)); - scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages)); + scope.Notifications.Publish(new ContentTreeChangeNotification( + content, + TreeChangeTypes.RefreshBranch, + variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : null, + variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : ["*"], + eventMessages)); if (culturesUnpublishing != null) { @@ -1701,7 +1706,12 @@ public class ContentService : RepositoryService, IContentService if (!branchOne) { scope.Notifications.Publish( - new ContentTreeChangeNotification(content, changeType, eventMessages)); + new ContentTreeChangeNotification( + content, + changeType, + variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : ["*"], + variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : null, + eventMessages)); scope.Notifications.Publish( new ContentPublishedNotification(content, eventMessages).WithState(notificationState)); } @@ -2150,7 +2160,6 @@ public class ContentService : RepositoryService, IContentService var results = new List(); var publishedDocuments = new List(); - IDictionary? initialNotificationState = null; using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { scope.WriteLock(Constants.Locks.ContentTree); @@ -2169,7 +2178,8 @@ public class ContentService : RepositoryService, IContentService } // deal with the branch root - if it fails, abort - PublishResult? result = PublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out initialNotificationState); + HashSet? culturesToPublish = shouldPublish(document); + PublishResult? result = PublishBranchItem(scope, document, culturesToPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out IDictionary? notificationState); if (result != null) { results.Add(result); @@ -2179,6 +2189,8 @@ public class ContentService : RepositoryService, IContentService } } + HashSet culturesPublished = culturesToPublish ?? []; + // deal with descendants // if one fails, abort its branch var exclude = new HashSet(); @@ -2204,12 +2216,14 @@ public class ContentService : RepositoryService, IContentService } // no need to check path here, parent has to be published here - result = PublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _); + culturesToPublish = shouldPublish(d); + result = PublishBranchItem(scope, d, culturesToPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _); if (result != null) { results.Add(result); if (result.Success) { + culturesPublished.UnionWith(culturesToPublish ?? []); continue; } } @@ -2226,9 +2240,15 @@ public class ContentService : RepositoryService, IContentService // trigger events for the entire branch // (SaveAndPublishBranchOne does *not* do it) + var variesByCulture = document.ContentType.VariesByCulture(); scope.Notifications.Publish( - new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages)); - scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(initialNotificationState)); + new ContentTreeChangeNotification( + document, + TreeChangeTypes.RefreshBranch, + variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"], + null, + eventMessages)); + scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState)); scope.Complete(); } @@ -2242,7 +2262,7 @@ public class ContentService : RepositoryService, IContentService private PublishResult? PublishBranchItem( ICoreScope scope, IContent document, - Func?> shouldPublish, + HashSet? culturesToPublish, Func, IReadOnlyCollection, bool> publishCultures, bool isRoot, @@ -2252,9 +2272,7 @@ public class ContentService : RepositoryService, IContentService IReadOnlyCollection allLangs, out IDictionary? initialNotificationState) { - HashSet? culturesToPublish = shouldPublish(document); - - initialNotificationState = null; + initialNotificationState = new Dictionary(); // we need to guard against unsaved changes before proceeding; the document will be saved, but we're not firing any saved notifications if (HasUnsavedChanges(document)) @@ -2797,6 +2815,13 @@ public class ContentService : RepositoryService, IContentService GetPagedDescendants(content.Id, page++, pageSize, out total); foreach (IContent descendant in descendants) { + // when copying a branch into itself, the copy of a root would be seen as a descendant + // and would be copied again => filter it out. + if (descendant.Id == copy.Id) + { + continue; + } + // if parent has not been copied, skip, else gets its copy id if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) { diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index f12daff4f2..3566696af4 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -4,12 +4,11 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Cms.Core.Services.Locking; -using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; @@ -28,7 +27,8 @@ public class ContentTypeService : ContentTypeServiceBase + userIdKeyResolver, + contentTypeFilters) => ContentService = contentService; [Obsolete("Use the ctor specifying all dependencies instead")] @@ -65,6 +66,32 @@ public class ContentTypeService : ContentTypeServiceBase()) { } + [Obsolete("Use the ctor specifying all dependencies instead")] + public ContentTypeService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IContentService contentService, + IContentTypeRepository repository, + IAuditRepository auditRepository, + IDocumentTypeContainerRepository entityContainerRepository, + IEntityRepository entityRepository, + IEventAggregator eventAggregator, + IUserIdKeyResolver userIdKeyResolver) + : this( + provider, + loggerFactory, + eventMessagesFactory, + contentService, + repository, + auditRepository, + entityContainerRepository, + entityRepository, + eventAggregator, + userIdKeyResolver, + StaticServiceProvider.Instance.GetRequiredService()) + { } + protected override int[] ReadLockIds => ContentTypeLocks.ReadLockIds; protected override int[] WriteLockIds => ContentTypeLocks.WriteLockIds; diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 171a0dcf00..46399f6b63 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; @@ -26,6 +27,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe private readonly IEntityRepository _entityRepository; private readonly IEventAggregator _eventAggregator; private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly ContentTypeFilterCollection _contentTypeFilters; protected ContentTypeServiceBase( ICoreScopeProvider provider, @@ -36,7 +38,8 @@ public abstract class ContentTypeServiceBase : ContentTypeSe IEntityContainerRepository containerRepository, IEntityRepository entityRepository, IEventAggregator eventAggregator, - IUserIdKeyResolver userIdKeyResolver) + IUserIdKeyResolver userIdKeyResolver, + ContentTypeFilterCollection contentTypeFilters) : base(provider, loggerFactory, eventMessagesFactory) { Repository = repository; @@ -45,6 +48,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe _entityRepository = entityRepository; _eventAggregator = eventAggregator; _userIdKeyResolver = userIdKeyResolver; + _contentTypeFilters = contentTypeFilters; } [Obsolete("Use the ctor specifying all dependencies instead")] @@ -70,6 +74,31 @@ public abstract class ContentTypeServiceBase : ContentTypeSe { } + [Obsolete("Use the ctor specifying all dependencies instead")] + protected ContentTypeServiceBase( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + TRepository repository, + IAuditRepository auditRepository, + IEntityContainerRepository containerRepository, + IEntityRepository entityRepository, + IEventAggregator eventAggregator, + IUserIdKeyResolver userIdKeyResolver) + : this( + provider, + loggerFactory, + eventMessagesFactory, + repository, + auditRepository, + containerRepository, + entityRepository, + eventAggregator, + userIdKeyResolver, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + protected TRepository Repository { get; } protected abstract int[] WriteLockIds { get; } protected abstract int[] ReadLockIds { get; } @@ -1130,7 +1159,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe #region Allowed types /// - public Task> GetAllAllowedAsRootAsync(int skip, int take) + public async Task> GetAllAllowedAsRootAsync(int skip, int take) { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); @@ -1140,28 +1169,39 @@ public abstract class ContentTypeServiceBase : ContentTypeSe IQuery query = ScopeProvider.CreateQuery().Where(x => x.AllowedAsRoot); IEnumerable contentTypes = Repository.Get(query).ToArray(); + foreach (IContentTypeFilter filter in _contentTypeFilters) + { + contentTypes = await filter.FilterAllowedAtRootAsync(contentTypes); + } + var pagedModel = new PagedModel { Total = contentTypes.Count(), Items = contentTypes.Skip(skip).Take(take) }; - return Task.FromResult(pagedModel); + return pagedModel; } /// - public Task?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take) + public async Task?, ContentTypeOperationStatus>> GetAllowedChildrenAsync(Guid key, int skip, int take) { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); TItem? parent = Get(key); if (parent?.AllowedContentTypes is null) { - return Task.FromResult(Attempt.FailWithStatus?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotFound, null)); + return Attempt.FailWithStatus?, ContentTypeOperationStatus>(ContentTypeOperationStatus.NotFound, null); + } + + IEnumerable allowedContentTypes = parent.AllowedContentTypes; + foreach (IContentTypeFilter filter in _contentTypeFilters) + { + allowedContentTypes = await filter.FilterAllowedChildrenAsync(allowedContentTypes, key); } PagedModel result; - if (parent.AllowedContentTypes.Any() is false) + if (allowedContentTypes.Any() is false) { // no content types allowed under parent result = new PagedModel @@ -1174,7 +1214,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe { // Get the sorted keys. Whilst we can't guarantee the order that comes back from GetMany, we can use // this to sort the resulting list of allowed children. - Guid[] sortedKeys = parent.AllowedContentTypes.OrderBy(x => x.SortOrder).Select(x => x.Key).ToArray(); + Guid[] sortedKeys = allowedContentTypes.OrderBy(x => x.SortOrder).Select(x => x.Key).ToArray(); TItem[] allowedChildren = GetMany(sortedKeys).ToArray(); result = new PagedModel @@ -1184,7 +1224,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe }; } - return Task.FromResult(Attempt.SucceedWithStatus?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result)); + return Attempt.SucceedWithStatus?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, result); } #endregion diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index 4c99ba87e3..09c2e434ea 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -525,6 +525,7 @@ public class DocumentUrlService : IDocumentUrlService } + [Obsolete("This method is obsolete and will be removed in future versions. Use IPublishedUrlInfoProvider.GetAllAsync instead.")] public async Task> ListUrlsAsync(Guid contentKey) { var result = new List(); diff --git a/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollection.cs b/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollection.cs new file mode 100644 index 0000000000..50d6757b5f --- /dev/null +++ b/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollection.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Services.Filters; + +/// +/// Defines an ordered collection of . +/// +public class ContentTypeFilterCollection : BuilderCollectionBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The collection items. + public ContentTypeFilterCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollectionBuilder.cs b/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollectionBuilder.cs new file mode 100644 index 0000000000..f1323543a9 --- /dev/null +++ b/src/Umbraco.Core/Services/Filters/ContentTypeFilterCollectionBuilder.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Services.Filters; + +/// +/// Builds an ordered collection of . +/// +public class ContentTypeFilterCollectionBuilder : OrderedCollectionBuilderBase +{ + /// + protected override ContentTypeFilterCollectionBuilder This => this; +} diff --git a/src/Umbraco.Core/Services/Filters/IContentTypeFilter.cs b/src/Umbraco.Core/Services/Filters/IContentTypeFilter.cs new file mode 100644 index 0000000000..e0f582723c --- /dev/null +++ b/src/Umbraco.Core/Services/Filters/IContentTypeFilter.cs @@ -0,0 +1,25 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services.Filters; + +/// +/// Defines methods for filtering content types after retrieval from the database. +/// +public interface IContentTypeFilter +{ + /// + /// Filters the content types retrieved for being allowed at the root. + /// + /// Retrieved collection of content types. + /// Filtered collection of content types. + Task> FilterAllowedAtRootAsync(IEnumerable contentTypes) + where TItem : IContentTypeComposition; + + /// + /// Filters the content types retrieved for being allowed as children of a parent content type. + /// + /// Retrieved collection of content types. + /// The parent content type key. + /// Filtered collection of content types. + Task> FilterAllowedChildrenAsync(IEnumerable contentTypes, Guid parentKey); +} diff --git a/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs b/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs index 4463733146..12d4c3e72c 100644 --- a/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs +++ b/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs @@ -12,9 +12,15 @@ namespace Umbraco.Cms.Core.Services; /// public interface IIndexedEntitySearchService { + [Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")] PagedModel Search(UmbracoObjectTypes objectType, string query, int skip = 0, int take = 100, bool ignoreUserStartNodes = false); // default implementation to avoid breaking changes falls back to old behaviour + [Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")] PagedModel Search(UmbracoObjectTypes objectType, string query, Guid? parentId, int skip = 0, int take = 100, bool ignoreUserStartNodes = false) => Search(objectType,query, skip, take, ignoreUserStartNodes); + + // default implementation to avoid breaking changes falls back to old behaviour + PagedModel Search(UmbracoObjectTypes objectType, string query, Guid? parentId, IEnumerable? contentTypeIds, int skip = 0, int take = 100, bool ignoreUserStartNodes = false) + => Search(objectType,query, skip, take, ignoreUserStartNodes); } diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index df3bde6ce1..4cb17e9fbc 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -23,6 +23,7 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase< /// /// /// + [Obsolete("Please use the skip & take instead of pageIndex & pageSize, scheduled for removal in v17")] IEnumerable GetAll( long pageIndex, int pageSize, diff --git a/src/Umbraco.Core/Services/MediaTypeService.cs b/src/Umbraco.Core/Services/MediaTypeService.cs index 359b4a99a9..e6dcf15939 100644 --- a/src/Umbraco.Core/Services/MediaTypeService.cs +++ b/src/Umbraco.Core/Services/MediaTypeService.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Cms.Core.Services.Locking; using Umbraco.Extensions; @@ -24,7 +25,8 @@ public class MediaTypeService : ContentTypeServiceBase MediaService = mediaService; + userIdKeyResolver, + contentTypeFilters) => MediaService = mediaService; [Obsolete("Use the constructor with all dependencies instead")] public MediaTypeService( @@ -61,6 +64,32 @@ public class MediaTypeService : ContentTypeServiceBase()) + { + } protected override int[] ReadLockIds => MediaTypeLocks.ReadLockIds; diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 6435bf5c5c..2c675063a2 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -791,7 +791,7 @@ namespace Umbraco.Cms.Core.Services scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification)); - Audit(AuditType.Save, 0, member.Id); + Audit(AuditType.Save, userId, member.Id); scope.Complete(); return OperationResult.Attempt.Succeed(evtMsgs); @@ -858,7 +858,7 @@ namespace Umbraco.Cms.Core.Services scope.WriteLock(Constants.Locks.MemberTree); DeleteLocked(scope, member, evtMsgs, deletingNotification.State); - Audit(AuditType.Delete, 0, member.Id); + Audit(AuditType.Delete, userId, member.Id); scope.Complete(); return OperationResult.Attempt.Succeed(evtMsgs); diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs index 9700f51890..8d62ee1ab8 100644 --- a/src/Umbraco.Core/Services/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Filters; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -15,6 +16,34 @@ public class MemberTypeService : ContentTypeServiceBase()) { - MemberService = memberService; - _memberTypeRepository = memberTypeRepository; } // beware! order is important to avoid deadlocks diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs index ccdd259644..08eda5fe74 100644 --- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs +++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs @@ -52,6 +52,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher _publishedUrlProvider = publishedUrlProvider; } + [Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")] public IEnumerable Search( string query, UmbracoEntityTypes entityType, @@ -60,6 +61,17 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false) + => Search(query, entityType, pageSize, pageIndex, out totalFound, null, searchFrom, ignoreUserStartNodes); + + public IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string[]? contentTypeAliases, + string? searchFrom = null, + bool ignoreUserStartNodes = false) { var sb = new StringBuilder(); @@ -141,6 +153,11 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher entityType); } + if (contentTypeAliases?.Any() is true) + { + sb.Append($"+({string.Join(" ", contentTypeAliases.Select(alias => $"{ExamineFieldNames.ItemTypeFieldName}:{alias}"))}) "); + } + if (!_examineManager.TryGetIndex(indexName, out IIndex? index)) { throw new InvalidOperationException("No index found by name " + indexName); diff --git a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj index 3e90b4649d..0a4c1114b7 100644 --- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj +++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs index ee043ee095..52777cd860 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs @@ -101,8 +101,9 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich // - non-#comment nodes // - non-#text nodes // - non-empty #text nodes + // - empty #text between inline elements (see #17037) HtmlNode[] childNodes = element.ChildNodes - .Where(c => c.Name != CommentNodeName && (c.Name != TextNodeName || string.IsNullOrWhiteSpace(c.InnerText) is false)) + .Where(c => c.Name != CommentNodeName && (c.Name != TextNodeName || c.NextSibling is not null || string.IsNullOrWhiteSpace(c.InnerText) is false)) .ToArray(); var tag = TagName(element); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index bd101ad37b..74fa4f374d 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -438,6 +438,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -449,6 +450,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); return builder; } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs index 5191d400e6..bcaa77da70 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs @@ -187,7 +187,7 @@ public class ExamineIndexRebuilder : IIndexRebuilder { // If an index exists but it has zero docs we'll consider it empty and rebuild IIndex[] indexes = (onlyEmptyIndexes - ? _examineManager.Indexes.Where(x => ShouldRebuild(x)) + ? _examineManager.Indexes.Where(ShouldRebuild) : _examineManager.Indexes).ToArray(); if (indexes.Length == 0) diff --git a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs index eb6ce0f01c..555059e385 100644 --- a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs +++ b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs @@ -8,6 +8,7 @@ namespace Umbraco.Cms.Infrastructure.Examine; /// public interface IBackOfficeExamineSearcher { + [Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")] IEnumerable Search( string query, UmbracoEntityTypes entityType, @@ -16,4 +17,16 @@ public interface IBackOfficeExamineSearcher out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false); + + // default implementation to avoid breaking changes falls back to old behaviour + IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string[]? contentTypeAliases, + string? searchFrom = null, + bool ignoreUserStartNodes = false) + => Search(query, entityType, pageSize, pageIndex, out totalFound, null, searchFrom, ignoreUserStartNodes); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index a93ae543a0..99d32f373d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -223,6 +223,19 @@ internal class DictionaryRepository : EntityRepositoryBase return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); } + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.UniqueId, ids); + } + + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertToEntity); + } } private class DictionaryByKeyRepository : SimpleGetRepository @@ -266,6 +279,19 @@ internal class DictionaryRepository : EntityRepositoryBase return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); } + + protected override IEnumerable PerformGetAll(params string[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.Key, ids); + } + + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertToEntity); + } } protected override IEnumerable PerformGetAll(params int[]? ids) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs index 02f40b2978..cd12e1921a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs @@ -2,8 +2,8 @@ // See LICENSE for more details. using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -26,8 +26,11 @@ public class MultipleValueEditor : DataValueEditor IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) => + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + { _jsonSerializer = jsonSerializer; + Validators.Add(new MultipleValueValidator()); + } /// /// When multiple values are selected a json array will be posted back so we need to format for storage in diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs index 288ab7c158..b1ec1d2678 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RadioButtonsPropertyEditor.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.PropertyEditors; @@ -35,4 +36,7 @@ public class RadioButtonsPropertyEditor : DataEditor /// protected override IConfigurationEditor CreateConfigurationEditor() => new ValueListConfigurationEditor(_ioHelper, _configurationEditorJsonSerializer); + + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs index 98e70a8c80..66d1af9c77 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -20,37 +18,17 @@ namespace Umbraco.Cms.Core.PropertyEditors; ValueEditorIsReusable = true)] public class TrueFalsePropertyEditor : DataEditor { - private readonly IIOHelper _ioHelper; - /// /// Initializes a new instance of the class. /// - [Obsolete("Please use the constructor taking all parameters. This constructor will be removed in V17.")] public TrueFalsePropertyEditor(IDataValueEditorFactory dataValueEditorFactory) - : this( - dataValueEditorFactory, - StaticServiceProvider.Instance.GetRequiredService()) - { - } - - /// - /// Initializes a new instance of the class. - /// - public TrueFalsePropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper) : base(dataValueEditorFactory) - { - _ioHelper = ioHelper; - SupportsReadOnly = true; - } + => SupportsReadOnly = true; /// protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - /// - protected override IConfigurationEditor CreateConfigurationEditor() => - new TrueFalseConfigurationEditor(_ioHelper); - internal class TrueFalsePropertyValueEditor : DataValueEditor { public TrueFalsePropertyValueEditor( diff --git a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs index 4955f17554..48bf98c7a3 100644 --- a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs +++ b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs @@ -117,13 +117,15 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle while (page * pageSize < total) { IEnumerable memberToRefresh = _memberService.GetAll( - page++, pageSize, out total, "LoginName", Direction.Ascending, + page * pageSize, pageSize, out total, "LoginName", Direction.Ascending, memberType.Alias); foreach (IMember c in memberToRefresh) { _umbracoIndexingHandler.ReIndexForMember(c); } + + page++; } } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs b/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs index 0f097df262..5ac85ad0fd 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs @@ -1,5 +1,7 @@ using Examine; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; @@ -12,11 +14,32 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService { private readonly IBackOfficeExamineSearcher _backOfficeExamineSearcher; private readonly IEntityService _entityService; + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; public IndexedEntitySearchService(IBackOfficeExamineSearcher backOfficeExamineSearcher, IEntityService entityService) + : this( + backOfficeExamineSearcher, + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public IndexedEntitySearchService( + IBackOfficeExamineSearcher backOfficeExamineSearcher, + IEntityService entityService, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService) { _backOfficeExamineSearcher = backOfficeExamineSearcher; _entityService = entityService; + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; } public PagedModel Search(UmbracoObjectTypes objectType, string query, int skip = 0, int take = 100, bool ignoreUserStartNodes = false) @@ -29,6 +52,16 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService int skip = 0, int take = 100, bool ignoreUserStartNodes = false) + => Search(objectType, query, parentId, null, skip, take, ignoreUserStartNodes); + + public PagedModel Search( + UmbracoObjectTypes objectType, + string query, + Guid? parentId, + IEnumerable? contentTypeIds, + int skip = 0, + int take = 100, + bool ignoreUserStartNodes = false) { UmbracoEntityTypes entityType = objectType switch { @@ -40,12 +73,24 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); + Guid[]? contentTypeIdsAsArray = contentTypeIds as Guid[] ?? contentTypeIds?.ToArray(); + var contentTypeAliases = contentTypeIdsAsArray?.Length > 0 + ? (entityType switch + { + UmbracoEntityTypes.Document => _contentTypeService.GetMany(contentTypeIdsAsArray).Select(x => x.Alias), + UmbracoEntityTypes.Media => _mediaTypeService.GetMany(contentTypeIdsAsArray).Select(x => x.Alias), + UmbracoEntityTypes.Member => _memberTypeService.GetMany(contentTypeIdsAsArray).Select(x => x.Alias), + _ => throw new NotSupportedException("This service only supports searching for documents, media and members") + }).ToArray() + : null; + IEnumerable searchResults = _backOfficeExamineSearcher.Search( query, entityType, pageSize, pageNumber, out var totalFound, + contentTypeAliases, ignoreUserStartNodes: ignoreUserStartNodes, searchFrom: parentId?.ToString()); diff --git a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs index 4fbf5ccde8..70d2663864 100644 --- a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs @@ -142,7 +142,10 @@ public abstract class UmbracoViewPage : RazorPage string.Format( ContentSettings.PreviewBadge, HostingEnvironment.ToAbsolute(Core.Constants.System.DefaultUmbracoPath), - Context.Request.GetEncodedUrl(), + System.Web.HttpUtility.HtmlEncode(Context.Request.GetEncodedUrl()), // Belt and braces - via a browser at least it doesn't seem possible to have anything other than + // a valid culture code provided in the querystring of this URL. + // But just to be sure of prevention of an XSS vulnterablity we'll HTML encode here too. + // An expected URL is untouched by this encoding. UmbracoContext.PublishedRequest?.PublishedContent?.Key); } else diff --git a/src/Umbraco.Web.UI.Client/devops/icons/index.js b/src/Umbraco.Web.UI.Client/devops/icons/index.js index a37ebf36db..266c7a6efd 100644 --- a/src/Umbraco.Web.UI.Client/devops/icons/index.js +++ b/src/Umbraco.Web.UI.Client/devops/icons/index.js @@ -46,7 +46,8 @@ const collectDictionaryIcons = async () => { const icon = { name: iconDef.name, - legacy: iconDef.legacy, + legacy: iconDef.legacy, // TODO: Deprecated, remove in v.17. + hidden: iconDef.legacy ?? iconDef.internal, fileName: iconFileName, svg, output: `${iconsOutputDirectory}/${iconFileName}.ts`, @@ -137,9 +138,11 @@ const collectDiskIcons = async (icons) => { // Only append not already defined icons: if (!icons.find((x) => x.name === iconName)) { + // remove legacy for v.17 (Deprecated) const icon = { name: iconName, legacy: true, + hidden: true, fileName: iconFileName, svg, output: `${iconsOutputDirectory}/${iconFileName}.ts`, @@ -172,11 +175,13 @@ const generateJS = (icons) => { const JSPath = `${moduleDirectory}/icons.ts`; const iconDescriptors = icons.map((icon) => { + // remove legacy for v.17 (Deprecated) return `{ name: "${icon.name}", ${icon.legacy ? 'legacy: true,' : ''} + ${icon.hidden ? 'hidden: true,' : ''} path: () => import("./icons/${icon.fileName}.js"), - }`.replace(/\t/g, ''); // Regex removes white space [NL] + }`.replace(/\t/g, '').replace(/^\s*[\r\n]/gm, ''); // Regex removes white space [NL] // + regex that removes empty lines. [NL] }); const content = `export default [${iconDescriptors.join(',')}];`; diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index d42c1dab93..52e78f1956 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@umbraco-cms/backoffice", - "version": "15.2.0-rc", + "version": "15.3.0-rc", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@umbraco-cms/backoffice", - "version": "15.2.0-rc", + "version": "15.3.0-rc", "license": "MIT", "workspaces": [ "./src/packages/*" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index ed634f0456..3fc044ac38 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "15.2.0-rc", + "version": "15.3.0-rc", "type": "module", "exports": { ".": null, diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts index 857064431b..6199e4ae3f 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts @@ -1,4 +1,5 @@ import type { UmbAppContextConfig } from './app-context-config.interface.js'; +import { UmbNetworkConnectionStatusManager } from './network-connection-status.manager.js'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; @@ -13,6 +14,8 @@ export class UmbAppContext extends UmbContextBase { this.#serverUrl = config.serverUrl; this.#backofficePath = config.backofficePath; this.#serverConnection = config.serverConnection; + + new UmbNetworkConnectionStatusManager(this); } getBackofficePath() { diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/network-connection-status.manager.ts b/src/Umbraco.Web.UI.Client/src/apps/app/network-connection-status.manager.ts new file mode 100644 index 0000000000..ad573a2464 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/apps/app/network-connection-status.manager.ts @@ -0,0 +1,52 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { + UMB_NOTIFICATION_CONTEXT, + type UmbNotificationContext, + type UmbNotificationHandler, +} from '@umbraco-cms/backoffice/notification'; + +export class UmbNetworkConnectionStatusManager extends UmbControllerBase { + #notificationContext?: UmbNotificationContext; + #offlineNotification?: UmbNotificationHandler; + + #localize = new UmbLocalizationController(this); + + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => { + this.#notificationContext = notificationContext; + }); + + window.addEventListener('online', () => this.#onOnline()); + window.addEventListener('offline', () => this.#onOffline()); + } + + #onOnline() { + this.#offlineNotification?.close(); + this.#notificationContext?.peek('positive', { + data: { + headline: this.#localize.term('speechBubbles_onlineHeadline'), + message: this.#localize.term('speechBubbles_onlineMessage'), + }, + }); + } + + #onOffline() { + this.#offlineNotification = this.#notificationContext?.stay('danger', { + data: { + headline: this.#localize.term('speechBubbles_offlineHeadline'), + message: this.#localize.term('speechBubbles_offlineMessage'), + }, + }); + } + + override destroy() { + this.#offlineNotification?.close(); + this.removeEventListener('online', () => this.#onOnline()); + this.removeEventListener('offline', () => this.#onOffline()); + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index b8022342c5..56f2fa1b81 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -123,6 +123,7 @@ export default { buttons: { clearSelection: 'Ryd valg', select: 'Vælg', + choose: 'Vælg', somethingElse: 'Gør noget andet', bold: 'Fed', deindent: 'Fortryd indryk afsnit', @@ -344,6 +345,9 @@ export default { clickToUpload: 'Klik for at uploade', orClickHereToUpload: 'eller klik her for at vælge filer', disallowedFileType: 'Kan ikke uploade denne fil, den har ikke en godkendt filtype', + disallowedMediaType: "Kan ikke uploade denne fil, mediatypen med alias '%0%' er ikke tilladt her", + invalidFileName: 'Kan ikke uploade denne fil, den har et ugyldigt filnavn', + invalidFileSize: 'Kan ikke uploade denne fil, den er for stor', maxFileSize: 'Maks filstørrelse er', mediaRoot: 'Medie rod', moveToSameFolderFailed: 'Overordnet og destinations mappe kan ikke være den samme', @@ -352,8 +356,6 @@ export default { dragAndDropYourFilesIntoTheArea: 'Træk dine filer ind i dropzonen for, at uploade dem til\n mediebiblioteket.\n ', uploadNotAllowed: 'Upload er ikke tiladt på denne lokation', - disallowedMediaType: "Cannot upload this file, the media type with alias '%0%' is not allowed here", - invalidFileName: 'Cannot upload this file, it does not have a valid file name', }, member: { createNewMember: 'Opret et nyt medlem', @@ -535,10 +537,14 @@ export default { linkToMedia: 'Link til medie', selectContentStartNode: 'Vælg startnode for indhold', selectMedia: 'Vælg medie', + chooseMedia: 'Vælg medie', + chooseMediaStartNode: 'Vælg startnode for medie', selectMediaType: 'Vælg medietype', selectIcon: 'Vælg ikon', selectItem: 'Vælg item', selectLink: 'Vælg link', + addLink: 'Tilføj Link', + updateLink: 'Opdater Link', selectMacro: 'Vælg makro', selectContent: 'Vælg indhold', selectContentType: 'Vælg indholdstype', @@ -546,12 +552,14 @@ export default { selectMember: 'Vælg medlem', selectMembers: 'Vælg medlemmer', selectMemberGroup: 'Vælg medlemsgruppe', + chooseMemberGroup: 'Vælg medlemsgruppe', selectMemberType: 'Vælg medlemstype', selectNode: 'Vælg node', selectLanguages: 'Vælg sprog', selectSections: 'Vælg sektioner', selectUser: 'Vælg bruger', selectUsers: 'Vælg brugere', + chooseUsers: 'Vælg brugere', noIconsFound: 'Ingen ikoner blev fundet', noMacroParams: 'Der er ingen parametre for denne makro', noMacros: 'Der er ikke tilføjet nogen makroer', @@ -1919,6 +1927,9 @@ export default { selectUserGroup: (multiple: boolean) => { return multiple ? 'Vælg brugergrupper' : 'Vælg brugergruppe'; }, + chooseUserGroup: (multiple: boolean) => { + return multiple ? 'Vælg brugergrupper' : 'Vælg brugergruppe'; + }, noStartNode: 'Ingen startnode valgt', noStartNodes: 'Ingen startnoder valgt', startnode: 'Indhold startnode', @@ -2031,14 +2042,16 @@ export default { entriesShort: 'Minimum %0% element(er), tilføj %1% mere.', entriesExceed: 'Maksimum %0% element(er), %1% for mange.', entriesAreasMismatch: 'Ét eller flere områder lever ikke op til kravene for antal indholdselementer.', - invalidMemberGroupName: 'Invalid member group name', - invalidUserGroupName: 'Invalid user group name', - invalidToken: 'Invalid token', - invalidUsername: 'Invalid username', - duplicateEmail: "Email '%0%' is already taken", - duplicateUserGroupName: "User group name '%0%' is already taken", - duplicateMemberGroupName: "Member group name '%0%' is already taken", - duplicateUsername: "Username '%0%' is already taken", + invalidMemberGroupName: 'Ugyldig medlemsgruppenavn', + invalidUserGroupName: 'Ugyldig brugergruppenavn', + invalidToken: 'Ugyldig token', + invalidUsername: 'Ugyldigt brugernavn', + duplicateEmail: "Email '%0%' er allerede i brug", + duplicateUserGroupName: "Brugergruppenavn '%0%' er allerede taget", + duplicateMemberGroupName: "Medlemsgruppenavn '%0%' er allerede taget", + duplicateUsername: "Brugernavnet '%0%' er allerede taget", + legacyOption: 'Ugyldig indstilling', + legacyOptionDescription: 'Denne indstilling understøttes ikke længere, vælg venligst noget andet', }, redirectUrls: { disableUrlTracker: 'Slå URL tracker fra', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index dd4d25caac..21862b3542 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -14,7 +14,7 @@ export default { actions: { assigndomain: 'Culture and Hostnames', auditTrail: 'Audit Trail', - browse: 'Browse Node', + browse: 'Browse', changeDataType: 'Change Data Type', changeDocType: 'Change Document Type', chooseWhereToCopy: 'Choose where to copy', @@ -129,6 +129,7 @@ export default { buttons: { clearSelection: 'Clear selection', select: 'Select', + choose: 'Choose', somethingElse: 'Do something else', bold: 'Bold', deindent: 'Cancel Paragraph Indent', @@ -375,6 +376,7 @@ export default { disallowedFileType: 'Cannot upload this file, it does not have an approved file type', disallowedMediaType: "Cannot upload this file, the media type with alias '%0%' is not allowed here", invalidFileName: 'Cannot upload this file, it does not have a valid file name', + invalidFileSize: 'Cannot upload this file, it is too large', maxFileSize: 'Max file size is', mediaRoot: 'Media root', createFolderFailed: 'Failed to create a folder under parent id %0%', @@ -502,9 +504,9 @@ export default { copiedItemOfItems: 'Copied %0% out of %1% items', }, defaultdialogs: { - nodeNameLinkPicker: 'Link title', + nodeNameLinkPicker: 'Title', urlLinkPicker: 'Link', - anchorLinkPicker: 'Anchor / querystring', + anchorLinkPicker: 'Anchor or querystring', anchorInsert: 'Name', closeThisWindow: 'Close this window', confirmdelete: 'Are you sure you want to delete', @@ -563,14 +565,17 @@ export default { includeDescendants: 'Include descendants', theFriendliestCommunity: 'The friendliest community', linkToPage: 'Link to document', - openInNewWindow: 'Opens the linked document in a new window or tab', + openInNewWindow: 'Opens the link in a new window or tab', linkToMedia: 'Link to media', selectContentStartNode: 'Select content start node', selectMedia: 'Select media', + chooseMediaStartNode: 'Choose Media Start nodes', selectMediaType: 'Select media type', selectIcon: 'Select icon', selectItem: 'Select item', - selectLink: 'Select link', + selectLink: 'Configure link', + addLink: 'Add Link', + updateLink: 'Update Link', selectMacro: 'Select macro', selectContent: 'Select content', selectContentType: 'Select content type', @@ -578,12 +583,14 @@ export default { selectMember: 'Choose member', selectMembers: 'Choose members', selectMemberGroup: 'Select member group', + chooseMemberGroup: 'Choose member group', selectMemberType: 'Select member type', selectNode: 'Select node', selectLanguages: 'Select languages', selectSections: 'Select sections', selectUser: 'Select user', selectUsers: 'Select users', + chooseUsers: 'Choose users', noIconsFound: 'No icons were found', noMacroParams: 'There are no parameters for this macro', noMacros: 'There are no macros available to insert', @@ -671,7 +678,7 @@ export default { email: 'Enter your email', enterMessage: 'Enter a message...', usernameHint: 'Your username is usually your email', - anchor: '#value or ?key=value', + anchor: 'Enter an anchor or querystring, #value or ?key=value', enterAlias: 'Enter alias...', generatingAlias: 'Generating alias...', a11yCreateItem: 'Create item', @@ -1485,6 +1492,8 @@ export default { preventCleanupDisableError: 'An error occurred while disabling version cleanup for %0%', copySuccessMessage: 'Your system information has successfully been copied to the clipboard', cannotCopyInformation: 'Could not copy your system information to the clipboard', + offlineHeadline: 'Offline', + offlineMessage: 'You are currently offline. Please check your internet connection.', }, stylesheet: { addRule: 'Add style', @@ -1971,6 +1980,9 @@ export default { selectUserGroup: (multiple: boolean) => { return multiple ? 'Select User Groups' : 'Select User Group'; }, + chooseUserGroup: (multiple: boolean) => { + return multiple ? 'Choose User Groups' : 'Choose User Group'; + }, noStartNode: 'No start node selected', noStartNodes: 'No start nodes selected', startnode: 'Content start node', 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 e5503b23bc..b6e9b4d434 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -13,7 +13,7 @@ export default { actions: { assigndomain: 'Culture and Hostnames', auditTrail: 'Audit Trail', - browse: 'Browse Node', + browse: 'Browse', changeDataType: 'Change Data Type', changeDocType: 'Change Document Type', chooseWhereToCopy: 'Choose where to copy', @@ -129,6 +129,7 @@ export default { buttons: { clearSelection: 'Clear selection', select: 'Select', + choose: 'Choose', somethingElse: 'Do something else', bold: 'Bold', deindent: 'Cancel Paragraph Indent', @@ -362,6 +363,7 @@ export default { disallowedFileType: 'Cannot upload this file, it does not have an approved file type', disallowedMediaType: "Cannot upload this file, the media type with alias '%0%' is not allowed here", invalidFileName: 'Cannot upload this file, it does not have a valid file name', + invalidFileSize: 'Cannot upload this file, it is too large', maxFileSize: 'Max file size is', mediaRoot: 'Media root', createFolderFailed: 'Failed to create a folder under parent id %0%', @@ -491,9 +493,9 @@ export default { copiedItemOfItems: 'Copied %0% out of %1% items', }, defaultdialogs: { - nodeNameLinkPicker: 'Link title', + nodeNameLinkPicker: 'Title', urlLinkPicker: 'Link', - anchorLinkPicker: 'Anchor / querystring', + anchorLinkPicker: 'Anchor or querystring', anchorInsert: 'Name', closeThisWindow: 'Close this window', confirmdelete: 'Are you sure you want to delete', @@ -553,15 +555,19 @@ export default { includeDescendants: 'Include descendants', theFriendliestCommunity: 'The friendliest community', linkToPage: 'Link to document', - openInNewWindow: 'Opens the linked document in a new window or tab', + openInNewWindow: 'Opens the link in a new window or tab', linkToMedia: 'Link to media', selectContentStartNode: 'Select content start node', selectEvent: 'Select event', selectMedia: 'Select media', + chooseMedia: 'Choose media', + chooseMediaStartNode: 'Choose Media Start nodes', selectMediaType: 'Select media type', selectIcon: 'Select icon', selectItem: 'Select item', - selectLink: 'Select link', + selectLink: 'Configure link', + addLink: 'Add Link', + updateLink: 'Update Link', selectMacro: 'Select macro', selectContent: 'Select content', selectContentType: 'Select content type', @@ -569,12 +575,14 @@ export default { selectMember: 'Choose member', selectMembers: 'Choose members', selectMemberGroup: 'Select member group', + chooseMemberGroup: 'Choose member group', selectMemberType: 'Select member type', selectNode: 'Select node', selectLanguages: 'Select languages', selectSections: 'Select sections', selectUser: 'Select user', selectUsers: 'Select users', + chooseUsers: 'Choose users', noIconsFound: 'No icons were found', noMacroParams: 'There are no parameters for this macro', noMacros: 'There are no macros available to insert', @@ -663,7 +671,7 @@ export default { email: 'Enter your email', enterMessage: 'Enter a message...', usernameHint: 'Your username is usually your email', - anchor: '#value or ?key=value', + anchor: 'Enter an anchor or querystring, #value or ?key=value', enterAlias: 'Enter alias...', generatingAlias: 'Generating alias...', a11yCreateItem: 'Create item', @@ -671,6 +679,7 @@ export default { a11yName: 'Name', rteParagraph: 'Write something amazing...', rteHeading: "What's the title?", + enterUrl: 'Enter a URL...', }, editcontenttype: { createListView: 'Create custom list view', @@ -798,6 +807,7 @@ export default { dictionary: 'Dictionary', dimensions: 'Dimensions', discard: 'Discard', + document: 'Document', down: 'Down', download: 'Download', edit: 'Edit', @@ -1486,6 +1496,10 @@ export default { scheduleErrExpireDate2: 'The expire date cannot be before the release date', preventCleanupEnableError: 'An error occurred while enabling version cleanup for %0%', preventCleanupDisableError: 'An error occurred while disabling version cleanup for %0%', + offlineHeadline: 'Offline', + offlineMessage: 'You are currently offline. Please check your internet connection.', + onlineHeadline: 'Online', + onlineMessage: 'You are now online. You can continue working.', }, stylesheet: { addRule: 'Add style', @@ -2008,6 +2022,9 @@ export default { selectUserGroup: (multiple: boolean) => { return multiple ? 'Select User Groups' : 'Select User Group'; }, + chooseUserGroup: (multiple: boolean) => { + return multiple ? 'Choose User Groups' : 'Choose User Group'; + }, noStartNode: 'No start node selected', noStartNodes: 'No start nodes selected', startnode: 'Content start node', @@ -2116,6 +2133,11 @@ export default { duplicateUserGroupName: "User group name '%0%' is already taken", duplicateMemberGroupName: "Member group name '%0%' is already taken", duplicateUsername: "Username '%0%' is already taken", + legacyOption: 'Legacy option', + legacyOptionDescription: 'This option is no longer supported, please select something else', + numberMinimum: "Value must be greater than or equal to '%0%'.", + numberMaximum: "Value must be less than or equal to '%0%'.", + numberMisconfigured: "Minimum value '%0%' must be less than the maximum value '%1%'.", }, healthcheck: { checkSuccessMessage: "Value is set to the recommended value: '%0%'.", @@ -2669,4 +2691,13 @@ export default { toolbar_removeItem: 'Remove action', toolbar_emptyGroup: 'Empty', }, + linkPicker: { + modalSource: 'Source', + modalManual: 'Manual', + modalAnchorValidationMessage: + 'Please enter an anchor or querystring, or select a published document or media item, or manually configure the URL.', + resetUrlHeadline: 'Reset URL?', + resetUrlMessage: 'Are you sure you want to reset this URL?', + resetUrlLabel: 'Reset', + }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/css/umb-css.css b/src/Umbraco.Web.UI.Client/src/css/umb-css.css index 0e612b10b6..5e783efb76 100644 --- a/src/Umbraco.Web.UI.Client/src/css/umb-css.css +++ b/src/Umbraco.Web.UI.Client/src/css/umb-css.css @@ -3,3 +3,9 @@ --umb-header-layout-height: 70px; --umb-section-sidebar-width: 300px; } + +@font-face{ + font-display:block; + font-family:codicon; + src:url('/umbraco/backoffice/assets/fonts/codicon/codicon.ttf') format("truetype") +} diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts index 851b36d512..773300fc9d 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts @@ -1494,6 +1494,7 @@ export class DocumentService { * @param data.skip * @param data.take * @param data.parentId + * @param data.allowedDocumentTypes * @returns unknown OK * @throws ApiError */ @@ -1505,7 +1506,8 @@ export class DocumentService { query: data.query, skip: data.skip, take: data.take, - parentId: data.parentId + parentId: data.parentId, + allowedDocumentTypes: data.allowedDocumentTypes }, errors: { 401: 'The resource is protected and requires an authentication token' @@ -3497,6 +3499,7 @@ export class MediaService { * @param data.skip * @param data.take * @param data.parentId + * @param data.allowedMediaTypes * @returns unknown OK * @throws ApiError */ @@ -3508,7 +3511,8 @@ export class MediaService { query: data.query, skip: data.skip, take: data.take, - parentId: data.parentId + parentId: data.parentId, + allowedMediaTypes: data.allowedMediaTypes }, errors: { 401: 'The resource is protected and requires an authentication token' @@ -4703,6 +4707,7 @@ export class MemberService { * @param data.query * @param data.skip * @param data.take + * @param data.allowedMemberTypes * @returns unknown OK * @throws ApiError */ @@ -4713,7 +4718,8 @@ export class MemberService { query: { query: data.query, skip: data.skip, - take: data.take + take: data.take, + allowedMemberTypes: data.allowedMemberTypes }, errors: { 401: 'The resource is protected and requires an authentication token' diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts index 2f2dfa87e7..055fd92749 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts @@ -470,6 +470,8 @@ export type CurrenUserConfigurationResponseModel = { */ usernameIsEmail: boolean; passwordConfiguration: (PasswordConfigurationResponseModel); + allowChangePassword: boolean; + allowTwoFactor: boolean; }; export type DatabaseInstallRequestModel = { @@ -3314,6 +3316,7 @@ export type GetItemDocumentData = { export type GetItemDocumentResponse = (Array<(DocumentItemResponseModel)>); export type GetItemDocumentSearchData = { + allowedDocumentTypes?: Array<(string)>; parentId?: string; query?: string; skip?: number; @@ -3882,6 +3885,7 @@ export type GetItemMediaData = { export type GetItemMediaResponse = (Array<(MediaItemResponseModel)>); export type GetItemMediaSearchData = { + allowedMediaTypes?: Array<(string)>; parentId?: string; query?: string; skip?: number; @@ -4233,6 +4237,7 @@ export type GetItemMemberData = { export type GetItemMemberResponse = (Array<(MemberItemResponseModel)>); export type GetItemMemberSearchData = { + allowedMemberTypes?: Array<(string)>; query?: string; skip?: number; take?: number; diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-div.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-div.extension.ts new file mode 100644 index 0000000000..86563b88c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-div.extension.ts @@ -0,0 +1,32 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +export interface DivOptions { + /** + * HTML attributes to add to the element. + * @default {} + * @example { class: 'foo' } + */ + HTMLAttributes: Record; +} + +export const Div = Node.create({ + name: 'div', + + priority: 50, + + group: 'block', + + content: 'inline*', + + addOptions() { + return { HTMLAttributes: {} }; + }, + + parseHTML() { + return [{ tag: 'div' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts new file mode 100644 index 0000000000..eb6b255c69 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts @@ -0,0 +1,52 @@ +import { Extension } from '@tiptap/core'; + +/** + * Converts camelCase to kebab-case. + * @param {string} str - The string to convert. + * @returns {string} The converted string. + */ +function camelCaseToKebabCase(str: string): string { + return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase()); +} + +export interface HtmlGlobalAttributesOptions { + /** + * The types where the text align attribute can be applied. + * @default [] + * @example ['heading', 'paragraph'] + */ + types: Array; +} + +export const HtmlGlobalAttributes = Extension.create({ + name: 'htmlGlobalAttributes', + + addOptions() { + return { types: [] }; + }, + + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + class: {}, + dataset: { + parseHTML: (element) => element.dataset, + renderHTML: (attributes) => { + const keys = attributes.dataset ? Object.keys(attributes.dataset) : []; + if (!keys.length) return {}; + const dataAtrrs: Record = {}; + keys.forEach((key) => { + dataAtrrs['data-' + camelCaseToKebabCase(key)] = attributes.dataset[key]; + }); + return dataAtrrs; + }, + }, + id: {}, + style: {}, + }, + }, + ]; + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts new file mode 100644 index 0000000000..084ae6e3d0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts @@ -0,0 +1,32 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +export interface SpanOptions { + /** + * HTML attributes to add to the element. + * @default {} + * @example { class: 'foo' } + */ + HTMLAttributes: Record; +} + +export const Span = Node.create({ + name: 'span', + + group: 'inline', + + inline: true, + + content: 'inline*', + + addOptions() { + return { HTMLAttributes: {} }; + }, + + parseHTML() { + return [{ tag: 'span' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts index 3a13add2cb..daa8071f26 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts @@ -8,6 +8,7 @@ export const umbEmbeddedMedia = Node.create({ inline() { return this.options.inline; }, + atom: true, marks: '', draggable: true, @@ -19,12 +20,18 @@ export const umbEmbeddedMedia = Node.create({ 'data-embed-height': { default: 240 }, 'data-embed-url': { default: null }, 'data-embed-width': { default: 360 }, - markup: { default: null }, + markup: { default: null, parseHTML: (element) => element.innerHTML }, }; }, parseHTML() { - return [{ tag: 'div', class: 'umb-embed-holder', getAttrs: (node) => ({ markup: node.innerHTML }) }]; + return [ + { + tag: 'div', + priority: 100, + getAttrs: (dom) => dom.classList.contains('umb-embed-holder') && null, + }, + ]; }, renderHTML({ HTMLAttributes }) { diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts index 16db5db676..68be91b9fe 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -28,8 +28,11 @@ export { TextAlign } from '@tiptap/extension-text-align'; export { Underline } from '@tiptap/extension-underline'; // CUSTOM EXTENSIONS -export * from './extensions/tiptap-umb-embedded-media.extension.js'; +export * from './extensions/tiptap-div.extension.js'; export * from './extensions/tiptap-figcaption.extension.js'; export * from './extensions/tiptap-figure.extension.js'; +export * from './extensions/tiptap-span.extension.js'; +export * from './extensions/tiptap-html-global-attributes.extension.js'; +export * from './extensions/tiptap-umb-embedded-media.extension.js'; export * from './extensions/tiptap-umb-image.extension.js'; export * from './extensions/tiptap-umb-link.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts index a0332b08a9..233bef12c9 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts @@ -175,6 +175,12 @@ describe('UmbLocalizeController', () => { expect((controller.term as any)('logout', 'Hello', 'World')).to.equal('Log out'); }); + it('should encode HTML entities', () => { + expect(controller.term('withInlineToken', 'Hello', ''), 'XSS detected').to.equal( + 'Hello <script>alert("XSS")</script>', + ); + }); + it('only reacts to changes of its own localization-keys', async () => { const element: UmbLocalizationRenderCountElement = await fixture( html``, diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts index d855db82a1..1c92265e91 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts @@ -20,6 +20,7 @@ import type { import { umbLocalizationManager } from './localization.manager.js'; import type { LitElement } from '@umbraco-cms/backoffice/external/lit'; import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { escapeHTML } from '@umbraco-cms/backoffice/utils'; const LocalizationControllerAlias = Symbol(); /** @@ -119,29 +120,35 @@ export class UmbLocalizationController escapeHTML(a)); + if (typeof term === 'function') { - return term(...args) as string; + return term(...sanitizedArgs) as string; } if (typeof term === 'string') { - if (args.length > 0) { + if (sanitizedArgs.length) { // Replace placeholders of format "%index%" and "{index}" with provided values term = term.replace(/(%(\d+)%|\{(\d+)\})/g, (match, _p1, p2, p3): string => { const index = p2 || p3; - return String(args[index] || match); + return typeof sanitizedArgs[index] !== 'undefined' ? String(sanitizedArgs[index]) : match; }); } } diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 9ca00d6970..0e8b63935a 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -406,6 +406,27 @@ export const data: Array = [ }, ], }, + { + name: 'Dropdown (Multiple)', + id: 'dt-dropdown-multiple', + parent: null, + editorAlias: 'Umbraco.DropDown.Flexible', + editorUiAlias: 'Umb.PropertyEditorUi.Dropdown', + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + values: [ + { + alias: 'multiple', + value: true, + }, + { + alias: 'items', + value: ['First Option', 'Second Option', 'I Am the third Option'], + }, + ], + }, { name: 'Dropdown Alignment Options', id: 'dt-dropdown-align', @@ -701,7 +722,70 @@ export const data: Array = [ values: [ { alias: 'fileExtensions', - value: ['jpg', 'jpeg', 'png', 'pdf'], + value: ['jpg', 'jpeg', 'png', 'svg'], + }, + { + alias: 'multiple', + value: true, + }, + ], + }, + { + name: 'Upload Field (Files)', + id: 'dt-uploadFieldFiles', + parent: null, + editorAlias: 'Umbraco.UploadField', + editorUiAlias: 'Umb.PropertyEditorUi.UploadField', + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + values: [ + { + alias: 'fileExtensions', + value: ['pdf', 'iso'], + }, + { + alias: 'multiple', + value: true, + }, + ], + }, + { + name: 'Upload Field (Movies)', + id: 'dt-uploadFieldMovies', + parent: null, + editorAlias: 'Umbraco.UploadField', + editorUiAlias: 'Umb.PropertyEditorUi.UploadField', + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + values: [ + { + alias: 'fileExtensions', + value: ['mp4', 'mov'], + }, + { + alias: 'multiple', + value: true, + }, + ], + }, + { + name: 'Upload Field (Vector)', + id: 'dt-uploadFieldVector', + parent: null, + editorAlias: 'Umbraco.UploadField', + editorUiAlias: 'Umb.PropertyEditorUi.UploadField', + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + values: [ + { + alias: 'fileExtensions', + value: ['svg'], }, { alias: 'multiple', @@ -891,8 +975,16 @@ export const data: Array = [ { alias: 'layouts', value: [ - { icon: 'icon-grid', isSystem: true, name: 'Grid', path: '', selected: true }, - { icon: 'icon-list', isSystem: true, name: 'Table', path: '', selected: true }, + { + icon: 'icon-grid', + name: 'Media Grid Collection View', + collectionView: 'Umb.CollectionView.Media.Grid', + }, + { + icon: 'icon-list', + name: 'Media Table Collection View', + collectionView: 'Umb.CollectionView.Media.Table', + }, ], }, { alias: 'icon', value: 'icon-layers' }, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts index e6a54130b6..540384e57c 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts @@ -337,6 +337,26 @@ export const data: Array = [ labelOnTop: false, }, }, + { + id: '19', + container: { id: 'all-properties-group-key' }, + alias: 'dropdownMultiple', + name: 'Dropdown (Multiple)', + description: '', + dataType: { id: 'dt-dropdown-multiple' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 11, + validation: { + mandatory: true, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, { id: '11', container: { id: 'all-properties-group-key' }, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index e3b159467f..cabad2395c 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -219,6 +219,13 @@ export const data: Array = [ segment: null, value: null, }, + { + editorAlias: 'Umbraco.DropDown.Flexible', + alias: 'dropdownMultiple', + culture: null, + segment: null, + value: null, + }, { editorAlias: 'Umbraco.TextArea', alias: 'textArea', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts index 389ec141ba..1614c94fc1 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.data.ts @@ -8,9 +8,14 @@ export type UmbMockMediaTypeModel = MediaTypeResponseModel & MediaTypeTreeItemResponseModel & MediaTypeItemResponseModel; +export type UmbMockMediaTypeUnionModel = + | MediaTypeResponseModel + | MediaTypeTreeItemResponseModel + | MediaTypeItemResponseModel; + export const data: Array = [ { - name: 'Media Type 1', + name: 'Image', id: 'media-type-1-id', parent: null, description: 'Media type 1 description', @@ -100,7 +105,7 @@ export const data: Array = [ aliasCanBeChanged: false, }, { - name: 'Media Type 2', + name: 'Audio', id: 'media-type-2-id', parent: null, description: 'Media type 2 description', @@ -113,7 +118,7 @@ export const data: Array = [ alias: 'umbracoFile', name: 'File', description: '', - dataType: { id: 'dt-uploadField' }, + dataType: { id: 'dt-uploadFieldFiles' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, @@ -150,7 +155,7 @@ export const data: Array = [ aliasCanBeChanged: false, }, { - name: 'Media Type 3', + name: 'Vector Graphics', id: 'media-type-3-id', parent: null, description: 'Media type 3 description', @@ -163,7 +168,7 @@ export const data: Array = [ alias: 'umbracoFile', name: 'File', description: '', - dataType: { id: 'dt-uploadField' }, + dataType: { id: 'dt-uploadFieldVector' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, @@ -200,7 +205,7 @@ export const data: Array = [ aliasCanBeChanged: false, }, { - name: 'Media Type 4', + name: 'Movie', id: 'media-type-4-id', parent: null, description: 'Media type 4 description', @@ -213,7 +218,7 @@ export const data: Array = [ alias: 'umbracoFile', name: 'File', description: '', - dataType: { id: 'dt-uploadField' }, + dataType: { id: 'dt-uploadFieldMovies' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, @@ -263,7 +268,7 @@ export const data: Array = [ alias: 'umbracoFile', name: 'File', description: '', - dataType: { id: 'dt-uploadField' }, + dataType: { id: 'dt-uploadFieldFiles' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.db.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.db.ts index de5e5765eb..6f271a453d 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.db.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media-type/media-type.db.ts @@ -3,19 +3,21 @@ import { UmbMockEntityFolderManager } from '../utils/entity/entity-folder.manage import { UmbMockEntityTreeManager } from '../utils/entity/entity-tree.manager.js'; import { UmbMockEntityItemManager } from '../utils/entity/entity-item.manager.js'; import { UmbMockEntityDetailManager } from '../utils/entity/entity-detail.manager.js'; -import type { UmbMockMediaTypeModel } from './media-type.data.js'; +import type { UmbMockMediaTypeModel, UmbMockMediaTypeUnionModel } from './media-type.data.js'; import { data } from './media-type.data.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { AllowedMediaTypeModel, CreateFolderRequestModel, CreateMediaTypeRequestModel, + GetItemMediaTypeAllowedResponse, MediaTypeItemResponseModel, MediaTypeResponseModel, MediaTypeSortModel, MediaTypeTreeItemResponseModel, PagedAllowedMediaTypeModel, } from '@umbraco-cms/backoffice/external/backend-api'; +import { umbDataTypeMockDb } from '../data-type/data-type.db.js'; class UmbMediaTypeMockDB extends UmbEntityMockDbBase { tree = new UmbMockEntityTreeManager(this, mediaTypeTreeItemMapper); @@ -45,6 +47,26 @@ class UmbMediaTypeMockDB extends UmbEntityMockDbBase { const mappedItems = mockItems.map((item) => allowedMediaTypeMapper(item)); return { items: mappedItems, total: mappedItems.length }; } + + getAllowedByFileExtension(fileExtension: string): GetItemMediaTypeAllowedResponse { + const allowedTypes = this.data.filter((field) => { + const allProperties = field.properties.flat(); + + const fileUploadType = allProperties.find((prop) => prop.alias === 'umbracoFile' || prop.alias === 'mediaPicker'); + if (!fileUploadType) return false; + + const dataType = umbDataTypeMockDb.read(fileUploadType.dataType.id); + if (dataType?.editorAlias !== 'Umbraco.UploadField') return false; + + const allowedFileExtensions = dataType.values.find((value) => value.alias === 'fileExtensions')?.value; + if (!allowedFileExtensions || !Array.isArray(allowedFileExtensions)) return false; + + return allowedFileExtensions.includes(fileExtension); + }); + + const mappedTypes = allowedTypes.map(mediaTypeItemMapper); + return allowedExtensionMediaTypeMapper(mappedTypes, mappedTypes.length); + } } const createMockMediaTypeFolderMapper = (request: CreateFolderRequestModel): UmbMockMediaTypeModel => { @@ -128,7 +150,7 @@ const mediaTypeTreeItemMapper = (item: UmbMockMediaTypeModel): MediaTypeTreeItem }; }; -const mediaTypeItemMapper = (item: UmbMockMediaTypeModel): MediaTypeItemResponseModel => { +const mediaTypeItemMapper = (item: UmbMockMediaTypeUnionModel): MediaTypeItemResponseModel => { return { id: item.id, name: item.name, @@ -145,4 +167,14 @@ const allowedMediaTypeMapper = (item: UmbMockMediaTypeModel): AllowedMediaTypeMo }; }; +const allowedExtensionMediaTypeMapper = ( + items: Array, + total: number, +): GetItemMediaTypeAllowedResponse => { + return { + items, + total, + }; +}; + export const umbMediaTypeMockDb = new UmbMediaTypeMockDB(data); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts index 01d0cdb071..f3a1ad9f9b 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/media/media.data.ts @@ -16,26 +16,38 @@ export const data: Array = [ isTrashed: false, mediaType: { id: 'media-type-1-id', - icon: 'icon-bug', + icon: 'icon-picture', }, values: [ + { + editorAlias: 'Umbraco.UploadField', + alias: 'mediaPicker', + value: { + src: '/umbraco/backoffice/assets/installer-illustration.svg', + }, + }, { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaHeadline', + alias: 'mediaType1Property1', value: 'The daily life at Umbraco HQ', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'Flipped Car', createDate: '2023-02-06T15:31:46.876902', updateDate: '2023-02-06T15:31:51.354764', }, ], - urls: [], + urls: [ + { + culture: null, + url: '/umbraco/backoffice/assets/installer-illustration.svg', + }, + ], }, { hasChildren: false, @@ -51,14 +63,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'Umbracoffee', createDate: '2023-02-06T15:31:46.876902', @@ -83,7 +95,7 @@ export const data: Array = [ variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'People', createDate: '2023-02-06T15:31:46.876902', @@ -108,7 +120,7 @@ export const data: Array = [ variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'John Smith', createDate: '2023-02-06T15:31:46.876902', @@ -131,14 +143,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'Jane Doe', createDate: '2023-02-06T15:31:46.876902', @@ -161,14 +173,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'John Doe', createDate: '2023-02-06T15:31:46.876902', @@ -191,14 +203,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'A very nice hat', createDate: '2023-02-06T15:31:46.876902', @@ -221,14 +233,14 @@ export const data: Array = [ values: [ { editorAlias: 'Umbraco.TextBox', - alias: 'myMediaDescription', + alias: 'mediaType1Property1', value: 'Every day, a rabbit in a military costume greets me at the front door', }, ], variants: [ { publishDate: '2023-02-06T15:31:51.354764', - culture: 'en-us', + culture: null, segment: null, name: 'Fancy old chair', createDate: '2023-02-06T15:31:46.876902', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts index a6ec2a5e7c..fdbf6cdb99 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts @@ -9,7 +9,6 @@ export const data: Array = [ name: 'Administrators', alias: 'admin', icon: 'icon-medal', - documentStartNode: { id: 'all-property-editors-document-id' }, fallbackPermissions: [ 'Umb.Document.Read', 'Umb.Document.Create', @@ -27,13 +26,7 @@ export const data: Array = [ 'Umb.Document.PublicAccess', 'Umb.Document.Rollback', ], - permissions: [ - { - $type: 'DocumentPermissionPresentationModel', - verbs: ['Umb.Document.Rollback'], - document: { id: 'simple-document-id' }, - }, - ], + permissions: [], sections: [ UMB_CONTENT_SECTION_ALIAS, 'Umb.Section.Media', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.db.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.db.ts index 5df763d89e..ee0a95b083 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.db.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.db.ts @@ -50,6 +50,16 @@ export class UmbUserGroupMockDB extends UmbEntityMockDbBase): string[] { + const fallbackPermissions = this.data + .filter((userGroup) => userGroupIds.map((reference) => reference.id).includes(userGroup.id)) + .map((userGroup) => (userGroup.fallbackPermissions?.length ? userGroup.fallbackPermissions : [])) + .flat(); + + // Remove duplicates + return Array.from(new Set(fallbackPermissions)); + } + getAllowedSections(userGroupIds: Array<{ id: string }>): string[] { const sections = this.data .filter((userGroup) => userGroupIds.map((reference) => reference.id).includes(userGroup.id)) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.db.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.db.ts index 231cb07ac7..7a4934357e 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.db.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.db.ts @@ -64,6 +64,9 @@ class UmbUserMockDB extends UmbEntityMockDbBase { getCurrentUser(): CurrentUserResponseModel { const firstUser = this.data[0]; const permissions = firstUser.userGroupIds?.length ? umbUserGroupMockDb.getPermissions(firstUser.userGroupIds) : []; + const fallbackPermissions = firstUser.userGroupIds?.length + ? umbUserGroupMockDb.getFallbackPermissions(firstUser.userGroupIds) + : []; const allowedSections = firstUser.userGroupIds?.length ? umbUserGroupMockDb.getAllowedSections(firstUser.userGroupIds) : []; @@ -82,7 +85,7 @@ class UmbUserMockDB extends UmbEntityMockDbBase { mediaStartNodeIds: firstUser.mediaStartNodeIds, hasDocumentRootAccess: firstUser.hasDocumentRootAccess, hasMediaRootAccess: firstUser.hasMediaRootAccess, - fallbackPermissions: [], + fallbackPermissions, permissions, allowedSections, isAdmin: firstUser.isAdmin, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-detail.manager.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-detail.manager.ts index 5ccbdec879..bc142e4f96 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-detail.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-detail.manager.ts @@ -22,7 +22,7 @@ export class UmbMockEntityDetailManager { return mockItem.id; } - read(id: string) { + read(id: string): MockType { const item = this.#db.read(id); if (!item) throw new Error('Item not found'); const mappedItem = this.#readResponseMapper(item); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/publishing.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/publishing.handlers.ts index 5678055cba..8c41ea34cd 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/publishing.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/publishing.handlers.ts @@ -2,6 +2,7 @@ const { rest } = window.MockServiceWorker; import { umbDocumentMockDb } from '../../data/document/document.db.js'; import { UMB_SLUG } from './slug.js'; import type { + GetDocumentByIdPublishedResponse, PublishDocumentRequestModel, UnpublishDocumentRequestModel, } from '@umbraco-cms/backoffice/external/backend-api'; @@ -25,4 +26,25 @@ export const publishingHandlers = [ umbDocumentMockDb.publishing.unpublish(id, requestBody); return res(ctx.status(200)); }), + + rest.get(umbracoPath(`${UMB_SLUG}/:id/published`), async (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return res(ctx.status(400)); + + const document = umbDocumentMockDb.detail.read(id); + + if (!document) return res(ctx.status(404)); + + const responseModel: GetDocumentByIdPublishedResponse = { + documentType: document.documentType, + id: document.id, + isTrashed: document.isTrashed, + urls: document.urls, + values: document.values, + variants: document.variants, + template: document.template, + }; + + return res(ctx.status(200), ctx.json(responseModel)); + }), ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media-type/item.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media-type/item.handlers.ts index cc1d570c26..e91ba1b3eb 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media-type/item.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media-type/item.handlers.ts @@ -10,4 +10,13 @@ export const itemHandlers = [ const items = umbMediaTypeMockDb.item.getItems(ids); return res(ctx.status(200), ctx.json(items)); }), + + rest.get(umbracoPath(`/item${UMB_SLUG}/allowed`), (req, res, ctx) => { + const fileExtension = req.url.searchParams.get('fileExtension'); + if (!fileExtension) return; + + const response = umbMediaTypeMockDb.getAllowedByFileExtension(fileExtension); + + return res(ctx.status(200), ctx.json(response)); + }), ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts index 96cb5a1ffc..fe52ac9a25 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/detail.handlers.ts @@ -8,6 +8,7 @@ import type { UpdateMediaRequestModel, } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +import type { UmbMediaDetailModel } from '@umbraco-cms/backoffice/media'; export const detailHandlers = [ rest.post(umbracoPath(`${UMB_SLUG}`), async (req, res, ctx) => { @@ -44,6 +45,23 @@ export const detailHandlers = [ return res(ctx.status(200), ctx.json(response)); }), + rest.put(umbracoPath(`${UMB_SLUG}/:id/validate`), async (req, res, ctx) => { + const id = req.params.id as string; + if (!id) return res(ctx.status(400)); + const model = await req.json(); + if (!model) return res(ctx.status(400)); + + const hasMediaPickerOrFileUploadValue = model.values.some((v) => { + return v.editorAlias === 'Umbraco.UploadField' && v.value; + }); + + if (!hasMediaPickerOrFileUploadValue) { + return res(ctx.status(400, 'No media picker or file upload value found')); + } + + return res(ctx.status(200)); + }), + rest.put(umbracoPath(`${UMB_SLUG}/:id`), async (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/imaging.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/imaging.handlers.ts new file mode 100644 index 0000000000..79ed72a98e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/imaging.handlers.ts @@ -0,0 +1,24 @@ +const { rest } = window.MockServiceWorker; +import { umbMediaMockDb } from '../../data/media/media.db.js'; +import type { GetImagingResizeUrlsResponse } from '@umbraco-cms/backoffice/external/backend-api'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const imagingHandlers = [ + rest.get(umbracoPath('/imaging/resize/urls'), (req, res, ctx) => { + const ids = req.url.searchParams.getAll('id'); + if (!ids) return res(ctx.status(404)); + + const media = umbMediaMockDb.getAll().filter((item) => ids.includes(item.id)); + + const response: GetImagingResizeUrlsResponse = media.map((item) => ({ + id: item.id, + urlInfos: item.urls, + })); + + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json(response), + ); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/index.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/index.ts index e8034da84d..3c5545f5f7 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/index.ts @@ -3,6 +3,7 @@ import { treeHandlers } from './tree.handlers.js'; import { itemHandlers } from './item.handlers.js'; import { detailHandlers } from './detail.handlers.js'; import { collectionHandlers } from './collection.handlers.js'; +import { imagingHandlers } from './imaging.handlers.js'; export const handlers = [ ...recycleBinHandlers, @@ -10,4 +11,5 @@ export const handlers = [ ...itemHandlers, ...detailHandlers, ...collectionHandlers, + ...imagingHandlers, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/tree.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/tree.handlers.ts index 308c04be52..53e112cba4 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/tree.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/media/tree.handlers.ts @@ -19,4 +19,11 @@ export const treeHandlers = [ const response = umbMediaMockDb.tree.getChildrenOf({ parentId, skip, take }); return res(ctx.status(200), ctx.json(response)); }), + + rest.get(umbracoPath(`/tree${UMB_SLUG}/ancestors`), (req, res, ctx) => { + const descendantId = req.url.searchParams.get('descendantId'); + if (!descendantId) return; + const response = umbMediaMockDb.tree.getAncestorsOf({ descendantId }); + return res(ctx.status(200), ctx.json(response)); + }), ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts index 07f3e8bcef..ee72270fee 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts @@ -1,12 +1,33 @@ const { rest } = window.MockServiceWorker; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; -import type { PostTemporaryFileResponse } from '@umbraco-cms/backoffice/external/backend-api'; +import type { + GetTemporaryFileConfigurationResponse, + PostTemporaryFileResponse, +} from '@umbraco-cms/backoffice/external/backend-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; const UMB_SLUG = 'temporary-file'; export const handlers = [ rest.post(umbracoPath(`/${UMB_SLUG}`), async (_req, res, ctx) => { - return res(ctx.delay(), ctx.status(201), ctx.text(UmbId.new())); + const guid = UmbId.new(); + return res( + ctx.delay(), + ctx.status(201), + ctx.set('Umb-Generated-Resource', guid), + ctx.text(guid), + ); + }), + + rest.get(umbracoPath(`/${UMB_SLUG}/configuration`), async (_req, res, ctx) => { + return res( + ctx.delay(), + ctx.json({ + allowedUploadedFileExtensions: [], + disallowedUploadedFilesExtensions: ['exe', 'dll', 'bat', 'msi'], + maxFileSize: 1468007, + imageFileTypes: ['jpg', 'png', 'gif', 'jpeg', 'svg'], + }), + ); }), ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.ts index 39da64181f..7dc42c91c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.ts @@ -24,6 +24,7 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator } #constructGridBlockValue(propertyValue: UmbBlockGridValueModel): UmbGridBlockClipboardEntryValueModel { + // TODO: investigate if structured can be remove here. const valueClone = structuredClone(propertyValue); const gridBlockValue: UmbGridBlockClipboardEntryValueModel = { @@ -38,7 +39,28 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator #constructBlockValue(propertyValue: UmbBlockGridValueModel): UmbBlockClipboardEntryValueModel { const gridBlockValue = this.#constructGridBlockValue(propertyValue); + const contentData: typeof gridBlockValue.contentData = []; + const settingsData: typeof gridBlockValue.settingsData = []; + const layout: UmbBlockClipboardEntryValueModel['layout'] = gridBlockValue.layout?.map((gridLayout) => { + const contentDataEntry = gridBlockValue.contentData.find( + (contentData) => contentData.key === gridLayout.contentKey, + ); + if (!contentDataEntry) { + throw new Error('No content data found for layout entry'); + } + contentData.push(contentDataEntry); + + if (gridLayout.settingsKey) { + const settingsDataEntry = gridBlockValue.settingsData.find( + (settingsData) => settingsData.key === gridLayout.settingsKey, + ); + if (!settingsDataEntry) { + throw new Error('No settings data found for layout entry'); + } + settingsData.push(settingsDataEntry); + } + return { contentKey: gridLayout.contentKey, settingsKey: gridLayout.settingsKey, @@ -46,9 +68,9 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator }); return { - contentData: gridBlockValue.contentData, layout: layout, - settingsData: gridBlockValue.settingsData, + contentData, + settingsData, }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.ts index a6483bd6b5..cd90e8e5bf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.ts @@ -1,8 +1,6 @@ import type { UmbBlockGridValueModel } from '../../../types.js'; import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../property-editors/constants.js'; import type { UmbGridBlockClipboardEntryValueModel } from '../../types.js'; -import { forEachBlockLayoutEntryOf } from '../../../utils/index.js'; -import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../../context/constants.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbClipboardCopyPropertyValueTranslator } from '@umbraco-cms/backoffice/clipboard'; @@ -10,19 +8,10 @@ export class UmbBlockGridToGridBlockClipboardCopyPropertyValueTranslator extends UmbControllerBase implements UmbClipboardCopyPropertyValueTranslator { - #blockGridManager?: typeof UMB_BLOCK_GRID_MANAGER_CONTEXT.TYPE; - async translate(propertyValue: UmbBlockGridValueModel) { if (!propertyValue) { throw new Error('Property value is missing.'); } - - this.#blockGridManager = await this.getContext(UMB_BLOCK_GRID_MANAGER_CONTEXT); - - return this.#constructGridBlockValue(propertyValue); - } - - #constructGridBlockValue(propertyValue: UmbBlockGridValueModel): UmbGridBlockClipboardEntryValueModel { const valueClone = structuredClone(propertyValue); const layout = valueClone.layout?.[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS] ?? undefined; @@ -33,24 +22,9 @@ export class UmbBlockGridToGridBlockClipboardCopyPropertyValueTranslator throw new Error('No layouts found.'); } - layout.forEach((layout) => { - // Find sub Blocks and append their data: - forEachBlockLayoutEntryOf(layout, async (entry) => { - const content = this.#blockGridManager!.getContentOf(entry.contentKey); - - if (!content) { - throw new Error('No content found'); - } - - contentData.push(structuredClone(content)); - - if (entry.settingsKey) { - const settings = this.#blockGridManager!.getSettingsOf(entry.settingsKey); - if (settings) { - settingsData.push(structuredClone(settings)); - } - } - }); + layout?.forEach((layoutItem) => { + // @ts-expect-error - We are removing the $type property from the layout item + delete layoutItem.$type; }); const gridBlockValue: UmbGridBlockClipboardEntryValueModel = { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts index 784e961ca9..aa92e88893 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts @@ -20,6 +20,8 @@ import { } from '@umbraco-cms/backoffice/extension-api'; import { UmbLanguageItemRepository } from '@umbraco-cms/backoffice/language'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; const apiArgsCreator: UmbApiConstructorArgumentsMethodType = (manifest: unknown) => { return [{ manifest }]; @@ -30,6 +32,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { // #blockContext?: typeof UMB_BLOCK_GRID_ENTRY_CONTEXT.TYPE; #workspaceContext?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; + #variantId: UmbVariantId | undefined; #contentKey?: string; #parentUnique?: string | null; #areaKey?: string | null; @@ -52,6 +55,9 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { @state() _inlineProperty?: UmbPropertyTypeModel; + @state() + _inlinePropertyDataPath?: string; + @state() private _ownerContentTypeName?: string; @@ -82,7 +88,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { UMB_BLOCK_WORKSPACE_ALIAS, apiArgsCreator, (permitted, ctrl) => { - const context = ctrl.api as typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; + const context = ctrl.api as typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE | undefined; if (permitted && context) { // Risky business, cause here we are lucky that it seems to be consumed and set before this is called and there for this is acceptable for now. [NL] if (this.#parentUnique === undefined || this.#areaKey === undefined) { @@ -101,6 +107,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { this.#workspaceContext.content.structure.contentTypeProperties, (contentTypeProperties) => { this._inlineProperty = contentTypeProperties[0]; + this.#generatePropertyDataPath(); }, 'observeProperties', ); @@ -116,9 +123,10 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { this.observe( context.variantId, async (variantId) => { + this.#variantId = variantId; + this.#generatePropertyDataPath(); if (variantId) { context.content.setup(this, variantId); - // TODO: Support segment name? const culture = variantId.culture; if (culture) { const languageRepository = new UmbLanguageItemRepository(this); @@ -142,6 +150,16 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { this.#workspaceContext.load(this.#contentKey); } + #generatePropertyDataPath() { + if (!this.#variantId || !this._inlineProperty) return; + const property = this._inlineProperty; + this._inlinePropertyDataPath = `$.values[${UmbDataPathPropertyValueQuery({ + alias: property.alias, + culture: property.variesByCulture ? this.#variantId!.culture : null, + segment: property.variesBySegment ? this.#variantId!.segment : null, + })}].value`; + } + #expose = () => { this.#workspaceContext?.expose(); }; @@ -189,6 +207,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { return html`
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts index 1afbe47805..495d1182ac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts @@ -31,7 +31,11 @@ function resolvePlacementAsBlockGrid( args: UmbSorterResolvePlacementArgs, ) { // If this has areas, we do not want to move, unless we are at the edge - if (args.relatedModel.areas?.length > 0 && isWithinRect(args.pointerX, args.pointerY, args.relatedRect, -10)) { + if ( + args.relatedModel.areas && + args.relatedModel.areas.length > 0 && + isWithinRect(args.pointerX, args.pointerY, args.relatedRect, -10) + ) { return null; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts index ce4343b722..5d71302070 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts @@ -1,4 +1,4 @@ -import { closestColumnSpanOption } from '../utils/index.js'; +import { closestColumnSpanOption, forEachBlockLayoutEntryOf } from '../utils/index.js'; import type { UmbBlockGridValueModel } from '../types.js'; import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS } from '../constants.js'; import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js'; @@ -296,6 +296,22 @@ export class UmbBlockGridEntryContext const settingsData = settings ? [structuredClone(settings)] : []; const exposes = expose ? [structuredClone(expose)] : []; + // Find sub Blocks and append their data: + forEachBlockLayoutEntryOf(layout, async (entry) => { + const content = this._manager!.getContentOf(entry.contentKey); + if (!content) { + throw new Error('No content found'); + } + contentData.push(structuredClone(content)); + + if (entry.settingsKey) { + const settings = this._manager!.getSettingsOf(entry.settingsKey); + if (settings) { + settingsData.push(structuredClone(settings)); + } + } + }); + const propertyValue: UmbBlockGridValueModel = { layout: { [UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]: layout ? [structuredClone(layout)] : undefined, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts index 20b42b6b18..ed58c645e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts @@ -132,14 +132,15 @@ export class UmbBlockGridManagerContext< // Lets check if we found the right parent layout entry: if (currentEntry.contentKey === parentId) { // Append the layout entry to be inserted and unfreeze the rest of the data: - const areas = currentEntry.areas.map((x) => - x.key === areaKey - ? { - ...x, - items: pushAtToUniqueArray([...x.items], insert, (x) => x.contentKey === insert.contentKey, index), - } - : x, - ); + const areas = + currentEntry.areas?.map((x) => + x.key === areaKey + ? { + ...x, + items: pushAtToUniqueArray([...x.items], insert, (x) => x.contentKey === insert.contentKey, index), + } + : x, + ) ?? []; return appendToFrozenArray( entries, { @@ -150,31 +151,33 @@ export class UmbBlockGridManagerContext< ); } // Otherwise check if any items of the areas are the parent layout entry we are looking for. We do so based on parentId, recursively: - let y: number = currentEntry.areas?.length; - while (y--) { - // Recursively ask the items of this area to insert the layout entry, if something returns there was a match in this branch. [NL] - const correctedAreaItems = this.#appendLayoutEntryToArea( - insert, - currentEntry.areas[y].items, - parentId, - areaKey, - index, - ); - if (correctedAreaItems) { - // This area got a corrected set of items, lets append those to the area and unfreeze the surrounding data: - const area = currentEntry.areas[y]; - return appendToFrozenArray( - entries, - { - ...currentEntry, - areas: appendToFrozenArray( - currentEntry.areas, - { ...area, items: correctedAreaItems }, - (z) => z.key === area.key, - ), - }, - (x) => x.contentKey === currentEntry.contentKey, + if (currentEntry.areas) { + let y: number = currentEntry.areas.length; + while (y--) { + // Recursively ask the items of this area to insert the layout entry, if something returns there was a match in this branch. [NL] + const correctedAreaItems = this.#appendLayoutEntryToArea( + insert, + currentEntry.areas[y].items, + parentId, + areaKey, + index, ); + if (correctedAreaItems) { + // This area got a corrected set of items, lets append those to the area and unfreeze the surrounding data: + const area = currentEntry.areas[y]; + return appendToFrozenArray( + entries, + { + ...currentEntry, + areas: appendToFrozenArray( + currentEntry.areas, + { ...area, items: correctedAreaItems }, + (z) => z.key === area.key, + ), + }, + (x) => x.contentKey === currentEntry.contentKey, + ); + } } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.test.ts index 274a525704..df10c51aa2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.test.ts @@ -49,7 +49,6 @@ describe('UmbBlockGridPropertyValueCloner', () => { { contentKey: 'content-3', settingsKey: 'settings-3', - areas: [], }, ], }, @@ -162,7 +161,7 @@ describe('UmbBlockGridPropertyValueCloner', () => { testLayoutEntryNewKeyIsReflected( 'fictive-content-type-2', - result.value?.layout[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]?.[0].areas[0]?.items[0], + result.value?.layout[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]?.[0].areas?.[0]?.items[0], result.value?.contentData, result.value?.settingsData, result.value?.expose, @@ -175,7 +174,8 @@ describe('UmbBlockGridPropertyValueCloner', () => { testLayoutEntryNewKeyIsReflected( 'fictive-content-type-3', - result.value?.layout[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]?.[0].areas[0]?.items[0].areas[0]?.items[0], + result.value?.layout[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]?.[0].areas?.[0]?.items[0].areas?.[0] + ?.items[0], result.value?.contentData, result.value?.settingsData, result.value?.expose, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.ts index cc996d332c..ac5ffb2eb0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.ts @@ -19,15 +19,17 @@ export class UmbBlockGridPropertyValueCloner extends UmbBlockPropertyValueCloner #cloneLayoutEntry = async (layout: UmbBlockGridLayoutModel): Promise => { // Clone the specific layout entry: const entryClone = await this._cloneBlock(layout); - // And then clone the items of its areas: - entryClone.areas = await Promise.all( - entryClone.areas.map(async (area) => { - return { - ...area, - items: await Promise.all(area.items.map(this.#cloneLayoutEntry)), - }; - }), - ); + if (entryClone.areas) { + // And then clone the items of its areas: + entryClone.areas = await Promise.all( + entryClone.areas.map(async (area) => { + return { + ...area, + items: await Promise.all(area.items.map(this.#cloneLayoutEntry)), + }; + }), + ); + } return entryClone; }; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/types.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/types.ts index de0f73b2a7..d4aa6406dd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/types.ts @@ -51,7 +51,7 @@ export interface UmbBlockGridValueModel extends UmbBlockValueType; + areas?: Array; } export interface UmbBlockGridLayoutAreaItemModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/Umbraco.BlockList.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/Umbraco.BlockList.ts index e946adef0b..45a4782f1a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/Umbraco.BlockList.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/Umbraco.BlockList.ts @@ -19,7 +19,7 @@ export const manifest: ManifestPropertyEditorSchema = { label: 'Amount', description: 'Set a required range of blocks', propertyEditorUiAlias: 'Umb.PropertyEditorUi.NumberRange', - config: [{ alias: 'validationRange', value: { min: 0, max: Infinity } }], + config: [{ alias: 'validationRange', value: { min: 0 } }], }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts index 1ed5d64637..c17d511676 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts @@ -1,5 +1,11 @@ import type { UmbBlockManagerContext, UmbBlockWorkspaceOriginData } from '../index.js'; -import type { UmbBlockLayoutBaseModel, UmbBlockDataModel, UmbBlockDataType, UmbBlockExposeModel } from '../types.js'; +import type { + UmbBlockLayoutBaseModel, + UmbBlockDataModel, + UmbBlockDataType, + UmbBlockExposeModel, + UmbBlockDataValueModel, +} from '../types.js'; import type { UmbBlockEntriesContext } from './block-entries.context.js'; import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; @@ -238,17 +244,7 @@ export abstract class UmbBlockEntryContext< if (!this.#contentValuesObservable) { this.#contentValuesObservable = mergeObservables( [this._contentValueArray, this.#contentStructure!.contentTypeProperties, this._variantId], - ([propertyValues, properties, variantId]) => { - if (!propertyValues || !properties || !variantId) return; - - return properties.reduce((acc, property) => { - const propertyVariantId = this.#createPropertyVariantId(property, variantId); - acc[property.alias] = propertyValues.find( - (x) => x.alias === property.alias && propertyVariantId.compare(x), - )?.value; - return acc; - }, {} as UmbBlockDataType); - }, + this.#propertyValuesToObjectCallback, ); } return this.#contentValuesObservable; @@ -274,21 +270,28 @@ export abstract class UmbBlockEntryContext< if (!this.#settingsValuesObservable) { this.#settingsValuesObservable = mergeObservables( [this._settingsValueArray, this.#settingsStructure!.contentTypeProperties, this._variantId], - ([propertyValues, properties, variantId]) => { - if (!propertyValues || !properties || !variantId) return; - - return properties.reduce((acc, property) => { - acc[property.alias] = propertyValues.find((x) => - this.#createPropertyVariantId(property, variantId).compare(x), - )?.value; - return acc; - }, {} as UmbBlockDataType); - }, + this.#propertyValuesToObjectCallback, ); } return this.#settingsValuesObservable; } + #propertyValuesToObjectCallback = ([propertyValues, properties, variantId]: [ + UmbBlockDataValueModel[] | undefined, + UmbPropertyTypeModel[], + UmbVariantId | undefined, + ]) => { + if (!propertyValues || !properties || !variantId) return; + + return properties.reduce((acc, property) => { + const propertyVariantId = this.#createPropertyVariantId(property, variantId); + acc[property.alias] = propertyValues.find( + (x) => x.alias === property.alias && propertyVariantId.compare(x), + )?.value; + return acc; + }, {} as UmbBlockDataType); + }; + /** * Get the settings of the block. * @returns {UmbBlockDataModel | undefined} - the settings of the block. diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context-token.ts index d83f7a1157..02d1adc11b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context-token.ts @@ -2,5 +2,6 @@ import type { UmbClipboardPropertyContext } from './clipboard.property-context.j import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export const UMB_CLIPBOARD_PROPERTY_CONTEXT = new UmbContextToken( + 'UmbPropertyContext', 'UmbClipboardPropertyContext', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context.ts index 48ce7b74ff..ce80e17c23 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/property/context/clipboard.property-context.ts @@ -15,9 +15,8 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UMB_PROPERTY_CONTEXT, UmbPropertyValueCloneController } from '@umbraco-cms/backoffice/property'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import type { ManifestPropertyEditorUi, UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor'; import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; -import { UMB_CONTEXT_REQUEST_EVENT_TYPE, type UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api'; /** * Clipboard context for managing clipboard entries for property values @@ -29,30 +28,15 @@ export class UmbClipboardPropertyContext extends UmbContextBase; #modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; - #propertyContext?: typeof UMB_PROPERTY_CONTEXT.TYPE; - #hostElement?: Element; - #propertyEditorElement?: UmbPropertyEditorUiElement; constructor(host: UmbControllerHost) { super(host, UMB_CLIPBOARD_PROPERTY_CONTEXT); - this.#hostElement = host.getHostElement(); - this.#init = Promise.all([ this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => { this.#modalManagerContext = context; }).asPromise(), - - this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { - this.#propertyContext = context; - this.#propertyEditorElement = context.getEditor(); - }).asPromise(), ]); - - this.#hostElement.addEventListener( - UMB_CONTEXT_REQUEST_EVENT_TYPE, - this.#proxyContextRequest.bind(this) as EventListener, - ); } /** @@ -143,7 +127,7 @@ export class UmbClipboardPropertyContext extends UmbContextBase 0; } - - #proxyContextRequest(event: UmbContextRequestEvent) { - const path = event.composedPath(); - - // Ignore events from the property editor element so we don't end up in a loop when proxying the requests. - if (path.includes(this.#propertyEditorElement as EventTarget)) { - return; - } - - // Proxy all context requests to the property editor element so the clipboard actions, translators and filters - // can consume contexts from property editor root element. - if (this.#propertyEditorElement) { - event.stopImmediatePropagation(); - this.#propertyEditorElement.dispatchEvent(event.clone()); - } - } - - override destroy(): void { - super.destroy(); - this.#hostElement?.removeEventListener( - UMB_CONTEXT_REQUEST_EVENT_TYPE, - this.#proxyContextRequest.bind(this) as EventListener, - ); - } } export { UmbClipboardPropertyContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/code-editor/code-editor-modal/code-editor-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/code-editor/code-editor-modal/code-editor-modal.element.ts index 93f4b59add..37d9415f58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/code-editor/code-editor-modal/code-editor-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/code-editor/code-editor-modal/code-editor-modal.element.ts @@ -1,11 +1,9 @@ import type { UmbCodeEditorElement } from '../components/code-editor.element.js'; import type { UmbCodeEditorModalData, UmbCodeEditorModalValue } from './code-editor-modal.token.js'; -import { css, html, ifDefined, customElement, query } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, ifDefined, query } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -const elementName = 'umb-code-editor-modal'; - -@customElement(elementName) +@customElement('umb-code-editor-modal') export class UmbCodeEditorModalElement extends UmbModalBaseElement { @query('umb-code-editor') _codeEditor?: UmbCodeEditorElement; @@ -18,29 +16,39 @@ export class UmbCodeEditorModalElement extends UmbModalBaseElement { + this._codeEditor?.editor?.monacoEditor?.getAction('editor.action.formatDocument')?.run(); + }, 100); + } + } + override render() { return html`
${this.#renderCodeEditor()}
-
- - -
+ +
`; } #renderCodeEditor() { return html` - + `; } @@ -63,6 +71,6 @@ export default UmbCodeEditorModalElement; declare global { interface HTMLElementTagNameMap { - [elementName]: UmbCodeEditorModalElement; + 'umb-code-editor-modal': UmbCodeEditorModalElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/code-editor/code-editor-modal/code-editor-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/code-editor/code-editor-modal/code-editor-modal.token.ts index d722df2d8d..f750ffcb78 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/code-editor/code-editor-modal/code-editor-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/code-editor/code-editor-modal/code-editor-modal.token.ts @@ -9,6 +9,7 @@ export interface UmbCodeEditorModalData { language: 'razor' | 'typescript' | 'javascript' | 'css' | 'markdown' | 'json' | 'html'; color?: 'positive' | 'danger'; confirmLabel?: string; + formatOnLoad?: boolean; } export interface UmbCodeEditorModalValue { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index a23d31729b..f22396f43f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -import { UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js'; +import { UMB_STORAGE_TOKEN_RESPONSE_NAME } from './constants.js'; import type { LocationLike, StringMap } from '@umbraco-cms/backoffice/external/openid'; import { BaseTokenRequestHandler, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.token.ts index 38907f35e4..4725be3acb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.token.ts @@ -2,5 +2,3 @@ import type { UmbAuthContext } from './auth.context.js'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; export const UMB_AUTH_CONTEXT = new UmbContextToken('UmbAuthContext'); -export const UMB_STORAGE_TOKEN_RESPONSE_NAME = 'umb:userAuthTokenResponse'; -export const UMB_STORAGE_REDIRECT_URL = 'umb:auth:redirect'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts index c0da4f0653..d4f6827120 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts @@ -1,7 +1,8 @@ import { UmbAuthFlow } from './auth-flow.js'; -import { UMB_AUTH_CONTEXT, UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js'; +import { UMB_AUTH_CONTEXT } from './auth.context.token.js'; import type { UmbOpenApiConfiguration } from './models/openApiConfiguration.js'; import type { ManifestAuthProvider } from './auth-provider.extension.js'; +import { UMB_STORAGE_TOKEN_RESPONSE_NAME } from './constants.js'; import { OpenAPI } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts index 54c99ef24c..4083ff7112 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts @@ -1,15 +1,17 @@ import type { UmbAuthProviderDefaultProps, UmbUserLoginState } from '../types.js'; -import { UmbLitElement } from '../../lit-element/lit-element.element.js'; -import { UmbTextStyles } from '../../style/index.js'; import type { ManifestAuthProvider } from '../auth-provider.extension.js'; import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @customElement('umb-auth-provider-default') export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbAuthProviderDefaultProps { @property({ attribute: false }) userLoginState?: UmbUserLoginState | undefined; + @property({ attribute: false }) manifest!: ManifestAuthProvider; + @property({ attribute: false }) onSubmit!: (manifestOrProviderName: string | ManifestAuthProvider, loginHint?: string) => void; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/constants.ts new file mode 100644 index 0000000000..10e5bdbb49 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/constants.ts @@ -0,0 +1 @@ +export const UMB_STORAGE_TOKEN_RESPONSE_NAME = 'umb:userAuthTokenResponse'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/index.ts index e7a2d3b39b..8468f04598 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/index.ts @@ -2,6 +2,7 @@ import './components/index.js'; export * from './auth.context.js'; export * from './auth.context.token.js'; +export * from './constants.js'; export * from './modals/index.js'; export type * from './models/openApiConfiguration.js'; export * from './components/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts index 98ebdd6dd6..d7fc98c2af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts @@ -80,7 +80,7 @@ export class UmbCollectionViewManager extends UmbControllerBase { if (views && views.length > 0) { // find the default view from the config. If it doesn't exist, use the first view const defaultView = views.find((view) => view.alias === this.#defaultViewAlias); - const fallbackView = defaultView?.meta.pathName || views[0].meta.pathName; + const fallbackView = defaultView ?? views[0]; routes = views.map((view) => { return { @@ -95,7 +95,10 @@ export class UmbCollectionViewManager extends UmbControllerBase { if (routes.length > 0) { routes.push({ path: '', - redirectTo: fallbackView, + component: () => createExtensionElement(fallbackView), + setup: () => { + this.setCurrentView(fallbackView); + }, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts index 83cf63b026..86315f5ce8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-number-range/input-number-range.element.ts @@ -123,7 +123,7 @@ export class UmbInputNumberRangeElement extends UmbFormControlMixin(UmbLitElemen label=${this.maxLabel} min=${ifDefined(this.validationRange?.min)} max=${ifDefined(this.validationRange?.max)} - placeholder=${this.validationRange?.max === Infinity ? '∞' : (this.validationRange?.max ?? '')} + placeholder=${this.validationRange?.max ?? '∞'} .value=${this._maxValue} @input=${this.#onMaxInput}> `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-radio-button-list/input-radio-button-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-radio-button-list/input-radio-button-list.element.ts index 211cd45d1d..747b5cb4bd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-radio-button-list/input-radio-button-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-radio-button-list/input-radio-button-list.element.ts @@ -1,10 +1,10 @@ -import { css, html, nothing, repeat, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, nothing, repeat, customElement, property, classMap } from '@umbraco-cms/backoffice/external/lit'; import { UUIFormControlMixin, UUIRadioElement } from '@umbraco-cms/backoffice/external/uui'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UUIRadioEvent } from '@umbraco-cms/backoffice/external/uui'; -type UmbRadioButtonItem = { label: string; value: string }; +export type UmbRadioButtonItem = { label: string; value: string; invalid?: boolean }; @customElement('umb-input-radio-button-list') export class UmbInputRadioButtonListElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -56,14 +56,24 @@ export class UmbInputRadioButtonListElement extends UUIFormControlMixin(UmbLitEl } #renderRadioButton(item: (typeof this.list)[0]) { - return html``; + return html``; } - static override styles = [ + static override readonly styles = [ css` :host { display: block; } + + uui-radio { + &.invalid { + text-decoration: line-through; + } + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts index 8cb1113c50..12787012c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts @@ -6,6 +6,9 @@ import type { UUISliderEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-input-slider') export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '') { + @property() + label: string = ''; + @property({ type: Number }) min = 0; @@ -50,6 +53,7 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '' #renderSlider() { return html` - - - + + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts index 4347644d1f..37d12fa9f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts @@ -5,6 +5,7 @@ export * from './constants.js'; export * from './entity-action-base.js'; export * from './entity-action-list.element.js'; export * from './entity-action.event.js'; +export * from './entity-updated.event.js'; export type * from './types.js'; export { UmbRequestReloadStructureForEntityEvent } from './request-reload-structure-for-entity.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json index 781b916fa2..1f25f205ba 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json @@ -641,7 +641,8 @@ { "name": "icon-document-dashed-line", "file": "file.svg", - "missing": "TODO:" + "missing": "TODO: Legacy until se have made a custom", + "legacy": true }, { "name": "icon-document", @@ -2360,6 +2361,11 @@ }, { "name": "icon-umb-manifest", + "file": "puzzle.svg", + "internal": true + }, + { + "name": "icon-puzzle-piece", "file": "puzzle.svg" }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-registry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-registry.context.ts index ec704b6ed0..b81e01b244 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-registry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-registry.context.ts @@ -13,7 +13,7 @@ export class UmbIconRegistryContext extends UmbContextBase([], (x) => x.name); readonly icons = this.#icons.asObservable(); - readonly approvedIcons = this.#icons.asObservablePart((icons) => icons.filter((x) => x.legacy !== true)); + readonly approvedIcons = this.#icons.asObservablePart((icons) => icons.filter((x) => x.hidden !== true)); constructor(host: UmbControllerHost) { super(host, UMB_ICON_REGISTRY_CONTEXT); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons.ts index 052c8c053a..ef2690f614 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons.ts @@ -1,2326 +1,2009 @@ export default [{ name: "icon-activity", - path: () => import("./icons/icon-activity.js"), },{ name: "icon-add", - path: () => import("./icons/icon-add.js"), },{ name: "icon-addressbook", - path: () => import("./icons/icon-addressbook.js"), },{ name: "icon-alarm-clock", - path: () => import("./icons/icon-alarm-clock.js"), },{ name: "icon-alert-alt", - path: () => import("./icons/icon-alert-alt.js"), },{ name: "icon-alert", - path: () => import("./icons/icon-alert.js"), },{ name: "icon-alt", - path: () => import("./icons/icon-alt.js"), },{ name: "icon-anchor", - path: () => import("./icons/icon-anchor.js"), },{ name: "icon-app", - path: () => import("./icons/icon-app.js"), },{ name: "icon-application-error", - path: () => import("./icons/icon-application-error.js"), },{ name: "icon-application-window-alt", - path: () => import("./icons/icon-application-window-alt.js"), },{ name: "icon-application-window", - path: () => import("./icons/icon-application-window.js"), },{ name: "icon-arrivals", - path: () => import("./icons/icon-arrivals.js"), },{ name: "icon-arrow-down", - path: () => import("./icons/icon-arrow-down.js"), },{ name: "icon-arrow-left", - path: () => import("./icons/icon-arrow-left.js"), },{ name: "icon-arrow-right", - path: () => import("./icons/icon-arrow-right.js"), },{ name: "icon-arrow-up", - path: () => import("./icons/icon-arrow-up.js"), },{ name: "icon-attachment", - path: () => import("./icons/icon-attachment.js"), },{ name: "icon-audio-lines", - path: () => import("./icons/icon-audio-lines.js"), },{ name: "icon-autofill", - path: () => import("./icons/icon-autofill.js"), },{ name: "icon-award", - path: () => import("./icons/icon-award.js"), },{ name: "icon-axis-rotation-2", - path: () => import("./icons/icon-axis-rotation-2.js"), },{ name: "icon-axis-rotation-3", - path: () => import("./icons/icon-axis-rotation-3.js"), },{ name: "icon-axis-rotation", - path: () => import("./icons/icon-axis-rotation.js"), },{ name: "icon-backspace", - path: () => import("./icons/icon-backspace.js"), },{ name: "icon-badge-add", - path: () => import("./icons/icon-badge-add.js"), },{ name: "icon-badge-remove", - path: () => import("./icons/icon-badge-remove.js"), },{ name: "icon-badge-restricted", legacy: true, +hidden: true, path: () => import("./icons/icon-badge-restricted.js"), },{ name: "icon-ball", - path: () => import("./icons/icon-ball.js"), },{ name: "icon-bar-chart", - path: () => import("./icons/icon-bar-chart.js"), },{ name: "icon-barcode", - path: () => import("./icons/icon-barcode.js"), },{ name: "icon-bars", - path: () => import("./icons/icon-bars.js"), },{ name: "icon-battery-full", - path: () => import("./icons/icon-battery-full.js"), },{ name: "icon-battery-low", - path: () => import("./icons/icon-battery-low.js"), },{ name: "icon-beer-glass", - path: () => import("./icons/icon-beer-glass.js"), },{ name: "icon-bell-off", - path: () => import("./icons/icon-bell-off.js"), },{ name: "icon-bell", - path: () => import("./icons/icon-bell.js"), },{ name: "icon-binarycode", - path: () => import("./icons/icon-binarycode.js"), },{ name: "icon-binoculars", legacy: true, +hidden: true, path: () => import("./icons/icon-binoculars.js"), },{ name: "icon-bird", - path: () => import("./icons/icon-bird.js"), },{ name: "icon-birthday-cake", - path: () => import("./icons/icon-birthday-cake.js"), },{ name: "icon-block", - path: () => import("./icons/icon-block.js"), },{ name: "icon-blockquote", - path: () => import("./icons/icon-blockquote.js"), },{ name: "icon-bluetooth", - path: () => import("./icons/icon-bluetooth.js"), },{ name: "icon-boat-shipping", - path: () => import("./icons/icon-boat-shipping.js"), },{ name: "icon-bold", - path: () => import("./icons/icon-bold.js"), },{ name: "icon-bones", - path: () => import("./icons/icon-bones.js"), },{ name: "icon-book-alt-2", - path: () => import("./icons/icon-book-alt-2.js"), },{ name: "icon-book-alt", - path: () => import("./icons/icon-book-alt.js"), },{ name: "icon-book", - path: () => import("./icons/icon-book.js"), },{ name: "icon-bookmark", - path: () => import("./icons/icon-bookmark.js"), },{ name: "icon-books", - path: () => import("./icons/icon-books.js"), },{ name: "icon-box-alt", - path: () => import("./icons/icon-box-alt.js"), },{ name: "icon-box-open", - path: () => import("./icons/icon-box-open.js"), },{ name: "icon-box", - path: () => import("./icons/icon-box.js"), },{ name: "icon-brackets", - path: () => import("./icons/icon-brackets.js"), },{ name: "icon-brick", - path: () => import("./icons/icon-brick.js"), },{ name: "icon-briefcase", - path: () => import("./icons/icon-briefcase.js"), },{ name: "icon-browser-window", - path: () => import("./icons/icon-browser-window.js"), },{ name: "icon-brush-alt-2", - path: () => import("./icons/icon-brush-alt-2.js"), },{ name: "icon-brush-alt", - path: () => import("./icons/icon-brush-alt.js"), },{ name: "icon-brush", - path: () => import("./icons/icon-brush.js"), },{ name: "icon-bug", - path: () => import("./icons/icon-bug.js"), },{ name: "icon-bulleted-list", - path: () => import("./icons/icon-bulleted-list.js"), },{ name: "icon-burn", - path: () => import("./icons/icon-burn.js"), },{ name: "icon-bus", - path: () => import("./icons/icon-bus.js"), },{ name: "icon-calculator", - path: () => import("./icons/icon-calculator.js"), },{ name: "icon-calendar-alt", - path: () => import("./icons/icon-calendar-alt.js"), },{ name: "icon-calendar", - path: () => import("./icons/icon-calendar.js"), },{ name: "icon-camcorder", legacy: true, +hidden: true, path: () => import("./icons/icon-camcorder.js"), },{ name: "icon-camera-roll", - path: () => import("./icons/icon-camera-roll.js"), },{ name: "icon-candy", - path: () => import("./icons/icon-candy.js"), },{ name: "icon-caps-lock", - path: () => import("./icons/icon-caps-lock.js"), },{ name: "icon-car", - path: () => import("./icons/icon-car.js"), },{ name: "icon-categories", - path: () => import("./icons/icon-categories.js"), },{ name: "icon-certificate", - path: () => import("./icons/icon-certificate.js"), },{ name: "icon-chart-curve", - path: () => import("./icons/icon-chart-curve.js"), },{ name: "icon-chart", - path: () => import("./icons/icon-chart.js"), },{ name: "icon-chat-active", legacy: true, +hidden: true, path: () => import("./icons/icon-chat-active.js"), },{ name: "icon-chat", - path: () => import("./icons/icon-chat.js"), },{ name: "icon-check", - path: () => import("./icons/icon-check.js"), },{ name: "icon-checkbox-dotted", - path: () => import("./icons/icon-checkbox-dotted.js"), },{ name: "icon-checkbox-empty", legacy: true, +hidden: true, path: () => import("./icons/icon-checkbox-empty.js"), },{ name: "icon-checkbox", - path: () => import("./icons/icon-checkbox.js"), },{ name: "icon-chip-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-chip-alt.js"), },{ name: "icon-chip", - path: () => import("./icons/icon-chip.js"), },{ name: "icon-cinema", - path: () => import("./icons/icon-cinema.js"), },{ name: "icon-circle-dotted-active", - path: () => import("./icons/icon-circle-dotted-active.js"), },{ name: "icon-circle-dotted", - path: () => import("./icons/icon-circle-dotted.js"), },{ name: "icon-circuits", - path: () => import("./icons/icon-circuits.js"), },{ name: "icon-clear-formatting", - path: () => import("./icons/icon-clear-formatting.js"), },{ name: "icon-client", legacy: true, +hidden: true, path: () => import("./icons/icon-client.js"), },{ name: "icon-clipboard", - path: () => import("./icons/icon-clipboard.js"), },{ name: "icon-clipboard-copy", - path: () => import("./icons/icon-clipboard-copy.js"), },{ name: "icon-clipboard-entry", - path: () => import("./icons/icon-clipboard-entry.js"), },{ name: "icon-clipboard-paste", - path: () => import("./icons/icon-clipboard-paste.js"), },{ name: "icon-cloud-drive", - path: () => import("./icons/icon-cloud-drive.js"), },{ name: "icon-cloud-upload", - path: () => import("./icons/icon-cloud-upload.js"), },{ name: "icon-cloud", - path: () => import("./icons/icon-cloud.js"), },{ name: "icon-cloudy", - path: () => import("./icons/icon-cloudy.js"), },{ name: "icon-clubs", - path: () => import("./icons/icon-clubs.js"), },{ name: "icon-cocktail", - path: () => import("./icons/icon-cocktail.js"), },{ name: "icon-code", - path: () => import("./icons/icon-code.js"), },{ name: "icon-code-xml", - path: () => import("./icons/icon-code-xml.js"), },{ name: "icon-coffee", - path: () => import("./icons/icon-coffee.js"), },{ name: "icon-coin-dollar", - path: () => import("./icons/icon-coin-dollar.js"), },{ name: "icon-coin-euro", - path: () => import("./icons/icon-coin-euro.js"), },{ name: "icon-coin-pound", - path: () => import("./icons/icon-coin-pound.js"), },{ name: "icon-coin-yen", - path: () => import("./icons/icon-coin-yen.js"), },{ name: "icon-coins-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-coins-alt.js"), },{ name: "icon-coins", - path: () => import("./icons/icon-coins.js"), },{ name: "icon-color-bucket", - path: () => import("./icons/icon-color-bucket.js"), },{ name: "icon-colorpicker", - path: () => import("./icons/icon-colorpicker.js"), },{ name: "icon-columns", - path: () => import("./icons/icon-columns.js"), },{ name: "icon-combination-lock-open", - path: () => import("./icons/icon-combination-lock-open.js"), },{ name: "icon-combination-lock", - path: () => import("./icons/icon-combination-lock.js"), },{ name: "icon-command", - path: () => import("./icons/icon-command.js"), },{ name: "icon-company", - path: () => import("./icons/icon-company.js"), },{ name: "icon-compress", - path: () => import("./icons/icon-compress.js"), },{ name: "icon-connection", - path: () => import("./icons/icon-connection.js"), },{ name: "icon-console", - path: () => import("./icons/icon-console.js"), },{ name: "icon-contrast", - path: () => import("./icons/icon-contrast.js"), },{ name: "icon-conversation-alt", - path: () => import("./icons/icon-conversation-alt.js"), },{ name: "icon-conversation", legacy: true, +hidden: true, path: () => import("./icons/icon-conversation.js"), },{ name: "icon-coverflow", - path: () => import("./icons/icon-coverflow.js"), },{ name: "icon-credit-card-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-credit-card-alt.js"), },{ name: "icon-credit-card", - path: () => import("./icons/icon-credit-card.js"), },{ name: "icon-crop", - path: () => import("./icons/icon-crop.js"), },{ name: "icon-crosshair", - path: () => import("./icons/icon-crosshair.js"), },{ name: "icon-crown-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-crown-alt.js"), },{ name: "icon-crown", - path: () => import("./icons/icon-crown.js"), },{ name: "icon-cupcake", legacy: true, +hidden: true, path: () => import("./icons/icon-cupcake.js"), },{ name: "icon-curve", - path: () => import("./icons/icon-curve.js"), },{ name: "icon-cut", - path: () => import("./icons/icon-cut.js"), },{ name: "icon-dashboard", - path: () => import("./icons/icon-dashboard.js"), },{ name: "icon-defrag", - path: () => import("./icons/icon-defrag.js"), },{ name: "icon-delete-key", - path: () => import("./icons/icon-delete-key.js"), },{ name: "icon-delete", - path: () => import("./icons/icon-delete.js"), },{ name: "icon-departure", - path: () => import("./icons/icon-departure.js"), },{ name: "icon-desktop", legacy: true, +hidden: true, path: () => import("./icons/icon-desktop.js"), },{ name: "icon-diagnostics", - path: () => import("./icons/icon-diagnostics.js"), },{ name: "icon-diagonal-arrow-alt", - path: () => import("./icons/icon-diagonal-arrow-alt.js"), },{ name: "icon-diagonal-arrow", - path: () => import("./icons/icon-diagonal-arrow.js"), },{ name: "icon-diamond", - path: () => import("./icons/icon-diamond.js"), },{ name: "icon-diamonds", - path: () => import("./icons/icon-diamonds.js"), },{ name: "icon-dice", - path: () => import("./icons/icon-dice.js"), },{ name: "icon-diploma-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-diploma-alt.js"), },{ name: "icon-diploma", - path: () => import("./icons/icon-diploma.js"), },{ name: "icon-directions-alt", - path: () => import("./icons/icon-directions-alt.js"), },{ name: "icon-directions", - path: () => import("./icons/icon-directions.js"), },{ name: "icon-disc", - path: () => import("./icons/icon-disc.js"), },{ name: "icon-disk-image", - path: () => import("./icons/icon-disk-image.js"), },{ name: "icon-display", - path: () => import("./icons/icon-display.js"), },{ name: "icon-dna", - path: () => import("./icons/icon-dna.js"), },{ name: "icon-dock-connector", - path: () => import("./icons/icon-dock-connector.js"), },{ name: "icon-document-dashed-line", - +legacy: true, +hidden: true, path: () => import("./icons/icon-document-dashed-line.js"), },{ name: "icon-document", - path: () => import("./icons/icon-document.js"), },{ name: "icon-documents", - path: () => import("./icons/icon-documents.js"), },{ name: "icon-donate", legacy: true, +hidden: true, path: () => import("./icons/icon-donate.js"), },{ name: "icon-door-open-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-door-open-alt.js"), },{ name: "icon-door-open", - path: () => import("./icons/icon-door-open.js"), },{ name: "icon-download-alt", - path: () => import("./icons/icon-download-alt.js"), },{ name: "icon-download", - path: () => import("./icons/icon-download.js"), },{ name: "icon-drop", - path: () => import("./icons/icon-drop.js"), },{ name: "icon-eco", - path: () => import("./icons/icon-eco.js"), },{ name: "icon-economy", legacy: true, +hidden: true, path: () => import("./icons/icon-economy.js"), },{ name: "icon-edit", - path: () => import("./icons/icon-edit.js"), },{ name: "icon-embed", - path: () => import("./icons/icon-embed.js"), },{ name: "icon-employee", legacy: true, +hidden: true, path: () => import("./icons/icon-employee.js"), },{ name: "icon-energy-saving-bulb", - path: () => import("./icons/icon-energy-saving-bulb.js"), },{ name: "icon-enter", - path: () => import("./icons/icon-enter.js"), },{ name: "icon-equalizer", - path: () => import("./icons/icon-equalizer.js"), },{ name: "icon-escape", - path: () => import("./icons/icon-escape.js"), },{ name: "icon-ethernet", - path: () => import("./icons/icon-ethernet.js"), },{ name: "icon-eye", - path: () => import("./icons/icon-eye.js"), },{ name: "icon-exit-fullscreen", - path: () => import("./icons/icon-exit-fullscreen.js"), },{ name: "icon-facebook-like", - path: () => import("./icons/icon-facebook-like.js"), },{ name: "icon-factory", - path: () => import("./icons/icon-factory.js"), },{ name: "icon-favorite", - path: () => import("./icons/icon-favorite.js"), },{ name: "icon-file-cabinet", - path: () => import("./icons/icon-file-cabinet.js"), },{ name: "icon-files", - path: () => import("./icons/icon-files.js"), },{ name: "icon-filter-arrows", - path: () => import("./icons/icon-filter-arrows.js"), },{ name: "icon-filter", - path: () => import("./icons/icon-filter.js"), },{ name: "icon-fingerprint", - path: () => import("./icons/icon-fingerprint.js"), },{ name: "icon-fire", - path: () => import("./icons/icon-fire.js"), },{ name: "icon-firewire", legacy: true, +hidden: true, path: () => import("./icons/icon-firewire.js"), },{ name: "icon-flag-alt", - path: () => import("./icons/icon-flag-alt.js"), },{ name: "icon-flag", - path: () => import("./icons/icon-flag.js"), },{ name: "icon-flash", - path: () => import("./icons/icon-flash.js"), },{ name: "icon-flashlight", - path: () => import("./icons/icon-flashlight.js"), },{ name: "icon-flowerpot", - path: () => import("./icons/icon-flowerpot.js"), },{ name: "icon-folder", - path: () => import("./icons/icon-folder.js"), },{ name: "icon-folders", - path: () => import("./icons/icon-folders.js"), },{ name: "icon-font", - path: () => import("./icons/icon-font.js"), },{ name: "icon-food", - path: () => import("./icons/icon-food.js"), },{ name: "icon-footprints", - path: () => import("./icons/icon-footprints.js"), },{ name: "icon-forking", - path: () => import("./icons/icon-forking.js"), },{ name: "icon-frame-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-frame-alt.js"), },{ name: "icon-frame", - path: () => import("./icons/icon-frame.js"), },{ name: "icon-fullscreen-alt", - path: () => import("./icons/icon-fullscreen-alt.js"), },{ name: "icon-fullscreen", - path: () => import("./icons/icon-fullscreen.js"), },{ name: "icon-game", - path: () => import("./icons/icon-game.js"), },{ name: "icon-geometry", legacy: true, +hidden: true, path: () => import("./icons/icon-geometry.js"), },{ name: "icon-gift", - path: () => import("./icons/icon-gift.js"), },{ name: "icon-glasses", - path: () => import("./icons/icon-glasses.js"), },{ name: "icon-globe-alt", - path: () => import("./icons/icon-globe-alt.js"), },{ name: "icon-globe-asia", legacy: true, +hidden: true, path: () => import("./icons/icon-globe-asia.js"), },{ name: "icon-globe-europe-africa", legacy: true, +hidden: true, path: () => import("./icons/icon-globe-europe-africa.js"), },{ name: "icon-globe-inverted-america", legacy: true, +hidden: true, path: () => import("./icons/icon-globe-inverted-america.js"), },{ name: "icon-globe-inverted-asia", legacy: true, +hidden: true, path: () => import("./icons/icon-globe-inverted-asia.js"), },{ name: "icon-globe-inverted-europe-africa", legacy: true, +hidden: true, path: () => import("./icons/icon-globe-inverted-europe-africa.js"), },{ name: "icon-globe", - path: () => import("./icons/icon-globe.js"), },{ name: "icon-gps", - path: () => import("./icons/icon-gps.js"), },{ name: "icon-graduate", - path: () => import("./icons/icon-graduate.js"), },{ name: "icon-grid", - path: () => import("./icons/icon-grid.js"), },{ name: "icon-hammer", - path: () => import("./icons/icon-hammer.js"), },{ name: "icon-hand-active-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-hand-active-alt.js"), },{ name: "icon-hand-active", - path: () => import("./icons/icon-hand-active.js"), },{ name: "icon-hand-pointer-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-hand-pointer-alt.js"), },{ name: "icon-hand-pointer", - path: () => import("./icons/icon-hand-pointer.js"), },{ name: "icon-handshake", - path: () => import("./icons/icon-handshake.js"), },{ name: "icon-handtool-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-handtool-alt.js"), },{ name: "icon-handtool", - path: () => import("./icons/icon-handtool.js"), },{ name: "icon-hard-drive-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-hard-drive-alt.js"), },{ name: "icon-hard-drive", legacy: true, +hidden: true, path: () => import("./icons/icon-hard-drive.js"), },{ name: "icon-heading-1", - path: () => import("./icons/icon-heading-1.js"), },{ name: "icon-heading-2", - path: () => import("./icons/icon-heading-2.js"), },{ name: "icon-heading-3", - path: () => import("./icons/icon-heading-3.js"), },{ name: "icon-headphones", - path: () => import("./icons/icon-headphones.js"), },{ name: "icon-headset", legacy: true, +hidden: true, path: () => import("./icons/icon-headset.js"), },{ name: "icon-hearts", - path: () => import("./icons/icon-hearts.js"), },{ name: "icon-height", - path: () => import("./icons/icon-height.js"), },{ name: "icon-help-alt", - path: () => import("./icons/icon-help-alt.js"), },{ name: "icon-help", - path: () => import("./icons/icon-help.js"), },{ name: "icon-history", - path: () => import("./icons/icon-history.js"), },{ name: "icon-home", - path: () => import("./icons/icon-home.js"), },{ name: "icon-horizontal-rule", - path: () => import("./icons/icon-horizontal-rule.js"), },{ name: "icon-hourglass", - path: () => import("./icons/icon-hourglass.js"), },{ name: "icon-imac", legacy: true, +hidden: true, path: () => import("./icons/icon-imac.js"), },{ name: "icon-image-up", - path: () => import("./icons/icon-image-up.js"), },{ name: "icon-inbox-full", legacy: true, +hidden: true, path: () => import("./icons/icon-inbox-full.js"), },{ name: "icon-inbox", - path: () => import("./icons/icon-inbox.js"), },{ name: "icon-indent", - path: () => import("./icons/icon-indent.js"), },{ name: "icon-infinity", - path: () => import("./icons/icon-infinity.js"), },{ name: "icon-info", - path: () => import("./icons/icon-info.js"), },{ name: "icon-invoice", legacy: true, +hidden: true, path: () => import("./icons/icon-invoice.js"), },{ name: "icon-ipad", legacy: true, +hidden: true, path: () => import("./icons/icon-ipad.js"), },{ name: "icon-iphone", legacy: true, +hidden: true, path: () => import("./icons/icon-iphone.js"), },{ name: "icon-italic", - path: () => import("./icons/icon-italic.js"), },{ name: "icon-item-arrangement", legacy: true, +hidden: true, path: () => import("./icons/icon-item-arrangement.js"), },{ name: "icon-junk", - path: () => import("./icons/icon-junk.js"), },{ name: "icon-key", - path: () => import("./icons/icon-key.js"), },{ name: "icon-keyboard", - path: () => import("./icons/icon-keyboard.js"), },{ name: "icon-lab", - path: () => import("./icons/icon-lab.js"), },{ name: "icon-laptop", - path: () => import("./icons/icon-laptop.js"), },{ name: "icon-layers-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-layers-alt.js"), },{ name: "icon-layers", - path: () => import("./icons/icon-layers.js"), },{ name: "icon-layout", - path: () => import("./icons/icon-layout.js"), },{ name: "icon-left-double-arrow", - path: () => import("./icons/icon-left-double-arrow.js"), },{ name: "icon-legal", - path: () => import("./icons/icon-legal.js"), },{ name: "icon-lense", legacy: true, +hidden: true, path: () => import("./icons/icon-lense.js"), },{ name: "icon-library", - path: () => import("./icons/icon-library.js"), },{ name: "icon-light-down", - path: () => import("./icons/icon-light-down.js"), },{ name: "icon-light-up", - path: () => import("./icons/icon-light-up.js"), },{ name: "icon-lightbulb-active", - path: () => import("./icons/icon-lightbulb-active.js"), },{ name: "icon-lightbulb", legacy: true, +hidden: true, path: () => import("./icons/icon-lightbulb.js"), },{ name: "icon-lightning", - path: () => import("./icons/icon-lightning.js"), },{ name: "icon-link", - path: () => import("./icons/icon-link.js"), },{ name: "icon-list", - path: () => import("./icons/icon-list.js"), },{ name: "icon-load", legacy: true, +hidden: true, path: () => import("./icons/icon-load.js"), },{ name: "icon-loading", legacy: true, +hidden: true, path: () => import("./icons/icon-loading.js"), },{ name: "icon-locate", - path: () => import("./icons/icon-locate.js"), },{ name: "icon-location-near-me", legacy: true, +hidden: true, path: () => import("./icons/icon-location-near-me.js"), },{ name: "icon-location-nearby", - path: () => import("./icons/icon-location-nearby.js"), },{ name: "icon-lock", - path: () => import("./icons/icon-lock.js"), },{ name: "icon-log-out", - path: () => import("./icons/icon-log-out.js"), },{ name: "icon-logout", legacy: true, +hidden: true, path: () => import("./icons/icon-logout.js"), },{ name: "icon-loupe", legacy: true, +hidden: true, path: () => import("./icons/icon-loupe.js"), },{ name: "icon-magnet", - path: () => import("./icons/icon-magnet.js"), },{ name: "icon-mailbox", - path: () => import("./icons/icon-mailbox.js"), },{ name: "icon-map-alt", - path: () => import("./icons/icon-map-alt.js"), },{ name: "icon-map-location", legacy: true, +hidden: true, path: () => import("./icons/icon-map-location.js"), },{ name: "icon-map-marker", - path: () => import("./icons/icon-map-marker.js"), },{ name: "icon-map", - path: () => import("./icons/icon-map.js"), },{ name: "icon-medal", - path: () => import("./icons/icon-medal.js"), },{ name: "icon-medical-emergency", - path: () => import("./icons/icon-medical-emergency.js"), },{ name: "icon-medicine", - path: () => import("./icons/icon-medicine.js"), },{ name: "icon-meeting", legacy: true, +hidden: true, path: () => import("./icons/icon-meeting.js"), },{ name: "icon-megaphone", - path: () => import("./icons/icon-megaphone.js"), },{ name: "icon-merge", - path: () => import("./icons/icon-merge.js"), },{ name: "icon-message-open", - path: () => import("./icons/icon-message-open.js"), },{ name: "icon-message-unopened", legacy: true, +hidden: true, path: () => import("./icons/icon-message-unopened.js"), },{ name: "icon-message", - path: () => import("./icons/icon-message.js"), },{ name: "icon-microscope", - path: () => import("./icons/icon-microscope.js"), },{ name: "icon-mindmap", legacy: true, +hidden: true, path: () => import("./icons/icon-mindmap.js"), },{ name: "icon-mobile", - path: () => import("./icons/icon-mobile.js"), },{ name: "icon-mountain", - path: () => import("./icons/icon-mountain.js"), },{ name: "icon-mouse-cursor", - path: () => import("./icons/icon-mouse-cursor.js"), },{ name: "icon-mouse", - path: () => import("./icons/icon-mouse.js"), },{ name: "icon-movie-alt", - path: () => import("./icons/icon-movie-alt.js"), },{ name: "icon-movie", - path: () => import("./icons/icon-movie.js"), },{ name: "icon-multiple-credit-cards", - path: () => import("./icons/icon-multiple-credit-cards.js"), },{ name: "icon-multiple-windows", - path: () => import("./icons/icon-multiple-windows.js"), },{ name: "icon-music", - path: () => import("./icons/icon-music.js"), },{ name: "icon-name-badge", legacy: true, +hidden: true, path: () => import("./icons/icon-name-badge.js"), },{ name: "icon-navigation-bottom", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-bottom.js"), },{ name: "icon-navigation-down", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-down.js"), },{ name: "icon-navigation-first", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-first.js"), },{ name: "icon-navigation-horizontal", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-horizontal.js"), },{ name: "icon-navigation-last", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-last.js"), },{ name: "icon-navigation-left", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-left.js"), },{ name: "icon-navigation-right", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-right.js"), },{ name: "icon-navigation-road", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-road.js"), },{ name: "icon-navigation-top", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-top.js"), },{ name: "icon-navigation-up", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-up.js"), },{ name: "icon-navigation-vertical", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation-vertical.js"), },{ name: "icon-navigation", legacy: true, +hidden: true, path: () => import("./icons/icon-navigation.js"), },{ name: "icon-navigational-arrow", - path: () => import("./icons/icon-navigational-arrow.js"), },{ name: "icon-network-alt", - path: () => import("./icons/icon-network-alt.js"), },{ name: "icon-newspaper-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-newspaper-alt.js"), },{ name: "icon-newspaper", - path: () => import("./icons/icon-newspaper.js"), },{ name: "icon-next-media", legacy: true, +hidden: true, path: () => import("./icons/icon-next-media.js"), },{ name: "icon-next", legacy: true, +hidden: true, path: () => import("./icons/icon-next.js"), },{ name: "icon-nodes", legacy: true, +hidden: true, path: () => import("./icons/icon-nodes.js"), },{ name: "icon-notepad-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-notepad-alt.js"), },{ name: "icon-notepad", - path: () => import("./icons/icon-notepad.js"), },{ name: "icon-old-key", - path: () => import("./icons/icon-old-key.js"), },{ name: "icon-old-phone", legacy: true, +hidden: true, path: () => import("./icons/icon-old-phone.js"), },{ name: "icon-operator", - path: () => import("./icons/icon-operator.js"), },{ name: "icon-ordered-list", - path: () => import("./icons/icon-ordered-list.js"), },{ name: "icon-origami", - path: () => import("./icons/icon-origami.js"), },{ name: "icon-out", - path: () => import("./icons/icon-out.js"), },{ name: "icon-outbox", legacy: true, +hidden: true, path: () => import("./icons/icon-outbox.js"), },{ name: "icon-outdent", - path: () => import("./icons/icon-outdent.js"), },{ name: "icon-page-add", - path: () => import("./icons/icon-page-add.js"), },{ name: "icon-page-down", - path: () => import("./icons/icon-page-down.js"), },{ name: "icon-page-remove", - path: () => import("./icons/icon-page-remove.js"), },{ name: "icon-page-restricted", - path: () => import("./icons/icon-page-restricted.js"), },{ name: "icon-page-up", - path: () => import("./icons/icon-page-up.js"), },{ name: "icon-paint-roller", legacy: true, +hidden: true, path: () => import("./icons/icon-paint-roller.js"), },{ name: "icon-palette", - path: () => import("./icons/icon-palette.js"), },{ name: "icon-panel-show", - path: () => import("./icons/icon-panel-show.js"), },{ name: "icon-pannel-close", - path: () => import("./icons/icon-pannel-close.js"), },{ name: "icon-paper-bag", legacy: true, +hidden: true, path: () => import("./icons/icon-paper-bag.js"), },{ name: "icon-paper-plane-alt", - path: () => import("./icons/icon-paper-plane-alt.js"), },{ name: "icon-paper-plane", - path: () => import("./icons/icon-paper-plane.js"), },{ name: "icon-partly-cloudy", - path: () => import("./icons/icon-partly-cloudy.js"), },{ name: "icon-paste-in", legacy: true, +hidden: true, path: () => import("./icons/icon-paste-in.js"), },{ name: "icon-pause", - path: () => import("./icons/icon-pause.js"), },{ name: "icon-pc", legacy: true, +hidden: true, path: () => import("./icons/icon-pc.js"), },{ name: "icon-people-alt-2", legacy: true, +hidden: true, path: () => import("./icons/icon-people-alt-2.js"), },{ name: "icon-people-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-people-alt.js"), },{ name: "icon-people-female", legacy: true, +hidden: true, path: () => import("./icons/icon-people-female.js"), },{ name: "icon-people", - path: () => import("./icons/icon-people.js"), },{ name: "icon-phone-ring", - path: () => import("./icons/icon-phone-ring.js"), },{ name: "icon-phone", - path: () => import("./icons/icon-phone.js"), },{ name: "icon-photo-album", - path: () => import("./icons/icon-photo-album.js"), },{ name: "icon-picture", - path: () => import("./icons/icon-picture.js"), },{ name: "icon-pictures-alt-2", - path: () => import("./icons/icon-pictures-alt-2.js"), },{ name: "icon-pictures-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-pictures-alt.js"), },{ name: "icon-pictures", - path: () => import("./icons/icon-pictures.js"), },{ name: "icon-pie-chart", - path: () => import("./icons/icon-pie-chart.js"), },{ name: "icon-piggy-bank", - path: () => import("./icons/icon-piggy-bank.js"), },{ name: "icon-pin-location", - path: () => import("./icons/icon-pin-location.js"), },{ name: "icon-plane", - path: () => import("./icons/icon-plane.js"), },{ name: "icon-planet", legacy: true, +hidden: true, path: () => import("./icons/icon-planet.js"), },{ name: "icon-play", - path: () => import("./icons/icon-play.js"), },{ name: "icon-playing-cards", legacy: true, +hidden: true, path: () => import("./icons/icon-playing-cards.js"), },{ name: "icon-playlist", - path: () => import("./icons/icon-playlist.js"), },{ name: "icon-plugin", - path: () => import("./icons/icon-plugin.js"), },{ name: "icon-podcast", - path: () => import("./icons/icon-podcast.js"), },{ name: "icon-poll", legacy: true, +hidden: true, path: () => import("./icons/icon-poll.js"), },{ name: "icon-post-it", - path: () => import("./icons/icon-post-it.js"), },{ name: "icon-power-outlet", legacy: true, +hidden: true, path: () => import("./icons/icon-power-outlet.js"), },{ name: "icon-power", - path: () => import("./icons/icon-power.js"), },{ name: "icon-presentation", - path: () => import("./icons/icon-presentation.js"), },{ name: "icon-previous-media", - path: () => import("./icons/icon-previous-media.js"), },{ name: "icon-previous", - path: () => import("./icons/icon-previous.js"), },{ name: "icon-price-dollar", legacy: true, +hidden: true, path: () => import("./icons/icon-price-dollar.js"), },{ name: "icon-price-euro", legacy: true, +hidden: true, path: () => import("./icons/icon-price-euro.js"), },{ name: "icon-price-pound", legacy: true, +hidden: true, path: () => import("./icons/icon-price-pound.js"), },{ name: "icon-price-yen", legacy: true, +hidden: true, path: () => import("./icons/icon-price-yen.js"), },{ name: "icon-print", - path: () => import("./icons/icon-print.js"), },{ name: "icon-printer-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-printer-alt.js"), },{ name: "icon-projector", - path: () => import("./icons/icon-projector.js"), },{ name: "icon-pulse", - path: () => import("./icons/icon-pulse.js"), },{ name: "icon-pushpin", - path: () => import("./icons/icon-pushpin.js"), },{ name: "icon-qr-code", - path: () => import("./icons/icon-qr-code.js"), },{ name: "icon-quote", - path: () => import("./icons/icon-quote.js"), },{ name: "icon-radio-alt", - path: () => import("./icons/icon-radio-alt.js"), },{ name: "icon-radio-receiver", - path: () => import("./icons/icon-radio-receiver.js"), },{ name: "icon-radio", - path: () => import("./icons/icon-radio.js"), },{ name: "icon-rain", - path: () => import("./icons/icon-rain.js"), },{ name: "icon-rate", legacy: true, +hidden: true, path: () => import("./icons/icon-rate.js"), },{ name: "icon-re-post", - path: () => import("./icons/icon-re-post.js"), },{ name: "icon-readonly", - path: () => import("./icons/icon-readonly.js"), },{ name: "icon-receipt-alt", - path: () => import("./icons/icon-receipt-alt.js"), },{ name: "icon-reception", - path: () => import("./icons/icon-reception.js"), },{ name: "icon-record", legacy: true, +hidden: true, path: () => import("./icons/icon-record.js"), },{ name: "icon-rectangle-ellipsis", - path: () => import("./icons/icon-rectangle-ellipsis.js"), },{ name: "icon-redo", - path: () => import("./icons/icon-redo.js"), },{ name: "icon-refresh", - path: () => import("./icons/icon-refresh.js"), },{ name: "icon-remote", legacy: true, +hidden: true, path: () => import("./icons/icon-remote.js"), },{ name: "icon-remove", - path: () => import("./icons/icon-remove.js"), },{ name: "icon-repeat-one", - path: () => import("./icons/icon-repeat-one.js"), },{ name: "icon-repeat", - path: () => import("./icons/icon-repeat.js"), },{ name: "icon-reply-arrow", - path: () => import("./icons/icon-reply-arrow.js"), },{ name: "icon-resize", - path: () => import("./icons/icon-resize.js"), },{ name: "icon-return-to-top", legacy: true, +hidden: true, path: () => import("./icons/icon-return-to-top.js"), },{ name: "icon-right-double-arrow", legacy: true, +hidden: true, path: () => import("./icons/icon-right-double-arrow.js"), },{ name: "icon-roadsign", legacy: true, +hidden: true, path: () => import("./icons/icon-roadsign.js"), },{ name: "icon-rocket", - path: () => import("./icons/icon-rocket.js"), },{ name: "icon-rss", - path: () => import("./icons/icon-rss.js"), },{ name: "icon-ruler-alt", - path: () => import("./icons/icon-ruler-alt.js"), },{ name: "icon-ruler", - path: () => import("./icons/icon-ruler.js"), },{ name: "icon-satellite-dish", - path: () => import("./icons/icon-satellite-dish.js"), },{ name: "icon-save", - path: () => import("./icons/icon-save.js"), },{ name: "icon-scan", - path: () => import("./icons/icon-scan.js"), },{ name: "icon-school", - path: () => import("./icons/icon-school.js"), },{ name: "icon-screensharing", - path: () => import("./icons/icon-screensharing.js"), },{ name: "icon-script-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-script-alt.js"), },{ name: "icon-script", - path: () => import("./icons/icon-script.js"), },{ name: "icon-scull", - path: () => import("./icons/icon-scull.js"), },{ name: "icon-search", - path: () => import("./icons/icon-search.js"), },{ name: "icon-sensor", - path: () => import("./icons/icon-sensor.js"), },{ name: "icon-server-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-server-alt.js"), },{ name: "icon-server", - path: () => import("./icons/icon-server.js"), },{ name: "icon-settings-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-settings-alt.js"), },{ name: "icon-settings", - path: () => import("./icons/icon-settings.js"), },{ name: "icon-share-alt", - path: () => import("./icons/icon-share-alt.js"), },{ name: "icon-share", - path: () => import("./icons/icon-share.js"), },{ name: "icon-sharing-iphone", - path: () => import("./icons/icon-sharing-iphone.js"), },{ name: "icon-shield", - path: () => import("./icons/icon-shield.js"), },{ name: "icon-shift", - path: () => import("./icons/icon-shift.js"), },{ name: "icon-shipping-box", - path: () => import("./icons/icon-shipping-box.js"), },{ name: "icon-shipping", - path: () => import("./icons/icon-shipping.js"), },{ name: "icon-shoe", legacy: true, +hidden: true, path: () => import("./icons/icon-shoe.js"), },{ name: "icon-shopping-basket-alt-2", legacy: true, +hidden: true, path: () => import("./icons/icon-shopping-basket-alt-2.js"), },{ name: "icon-shopping-basket-alt", - path: () => import("./icons/icon-shopping-basket-alt.js"), },{ name: "icon-shopping-basket", - path: () => import("./icons/icon-shopping-basket.js"), },{ name: "icon-shuffle", - path: () => import("./icons/icon-shuffle.js"), },{ name: "icon-sience", legacy: true, +hidden: true, path: () => import("./icons/icon-sience.js"), },{ name: "icon-science", - path: () => import("./icons/icon-science.js"), },{ name: "icon-single-note", - path: () => import("./icons/icon-single-note.js"), },{ name: "icon-sitemap", legacy: true, +hidden: true, path: () => import("./icons/icon-sitemap.js"), },{ name: "icon-sleep", - path: () => import("./icons/icon-sleep.js"), },{ name: "icon-slideshow", legacy: true, +hidden: true, path: () => import("./icons/icon-slideshow.js"), },{ name: "icon-smiley-inverted", legacy: true, +hidden: true, path: () => import("./icons/icon-smiley-inverted.js"), },{ name: "icon-smiley", - path: () => import("./icons/icon-smiley.js"), },{ name: "icon-snow", - path: () => import("./icons/icon-snow.js"), },{ name: "icon-sound-low", - path: () => import("./icons/icon-sound-low.js"), },{ name: "icon-sound-medium", legacy: true, +hidden: true, path: () => import("./icons/icon-sound-medium.js"), },{ name: "icon-sound-off", - path: () => import("./icons/icon-sound-off.js"), },{ name: "icon-sound-waves", - path: () => import("./icons/icon-sound-waves.js"), },{ name: "icon-sound", - path: () => import("./icons/icon-sound.js"), },{ name: "icon-spades", - path: () => import("./icons/icon-spades.js"), },{ name: "icon-speaker", - path: () => import("./icons/icon-speaker.js"), },{ name: "icon-speed-gauge", - path: () => import("./icons/icon-speed-gauge.js"), },{ name: "icon-split-alt", - path: () => import("./icons/icon-split-alt.js"), },{ name: "icon-split", - path: () => import("./icons/icon-split.js"), },{ name: "icon-sprout", - path: () => import("./icons/icon-sprout.js"), },{ name: "icon-squiggly-line", legacy: true, +hidden: true, path: () => import("./icons/icon-squiggly-line.js"), },{ name: "icon-ssd", legacy: true, +hidden: true, path: () => import("./icons/icon-ssd.js"), },{ name: "icon-stacked-disks", legacy: true, +hidden: true, path: () => import("./icons/icon-stacked-disks.js"), },{ name: "icon-stamp", legacy: true, +hidden: true, path: () => import("./icons/icon-stamp.js"), },{ name: "icon-stop-alt", - path: () => import("./icons/icon-stop-alt.js"), },{ name: "icon-stop-hand", legacy: true, +hidden: true, path: () => import("./icons/icon-stop-hand.js"), },{ name: "icon-stop", - path: () => import("./icons/icon-stop.js"), },{ name: "icon-store", - path: () => import("./icons/icon-store.js"), },{ name: "icon-stream", legacy: true, +hidden: true, path: () => import("./icons/icon-stream.js"), },{ name: "icon-strikethrough", - path: () => import("./icons/icon-strikethrough.js"), },{ name: "icon-subscript", - path: () => import("./icons/icon-subscript.js"), },{ name: "icon-superscript", - path: () => import("./icons/icon-superscript.js"), },{ name: "icon-sunny", - path: () => import("./icons/icon-sunny.js"), },{ name: "icon-sweatshirt", legacy: true, +hidden: true, path: () => import("./icons/icon-sweatshirt.js"), },{ name: "icon-sync", - path: () => import("./icons/icon-sync.js"), },{ name: "icon-t-shirt", - path: () => import("./icons/icon-t-shirt.js"), },{ name: "icon-tab-key", - path: () => import("./icons/icon-tab-key.js"), },{ name: "icon-table", - path: () => import("./icons/icon-table.js"), },{ name: "icon-tag", - path: () => import("./icons/icon-tag.js"), },{ name: "icon-tags", - path: () => import("./icons/icon-tags.js"), },{ name: "icon-takeaway-cup", legacy: true, +hidden: true, path: () => import("./icons/icon-takeaway-cup.js"), },{ name: "icon-target", - path: () => import("./icons/icon-target.js"), },{ name: "icon-temperatrure-alt", - path: () => import("./icons/icon-temperatrure-alt.js"), },{ name: "icon-temperature", - path: () => import("./icons/icon-temperature.js"), },{ name: "icon-terminal", - path: () => import("./icons/icon-terminal.js"), },{ name: "icon-text-align-center", - path: () => import("./icons/icon-text-align-center.js"), },{ name: "icon-text-align-justify", - path: () => import("./icons/icon-text-align-justify.js"), },{ name: "icon-text-align-left", - path: () => import("./icons/icon-text-align-left.js"), },{ name: "icon-text-align-right", - path: () => import("./icons/icon-text-align-right.js"), },{ name: "icon-theater", - path: () => import("./icons/icon-theater.js"), },{ name: "icon-thumb-down", - path: () => import("./icons/icon-thumb-down.js"), },{ name: "icon-thumb-up", - path: () => import("./icons/icon-thumb-up.js"), },{ name: "icon-thumbnail-list", - path: () => import("./icons/icon-thumbnail-list.js"), },{ name: "icon-thumbnails-small", - path: () => import("./icons/icon-thumbnails-small.js"), },{ name: "icon-thumbnails", - path: () => import("./icons/icon-thumbnails.js"), },{ name: "icon-ticket", - path: () => import("./icons/icon-ticket.js"), },{ name: "icon-time", - path: () => import("./icons/icon-time.js"), },{ name: "icon-timer", - path: () => import("./icons/icon-timer.js"), },{ name: "icon-tools", legacy: true, +hidden: true, path: () => import("./icons/icon-tools.js"), },{ name: "icon-top", legacy: true, +hidden: true, path: () => import("./icons/icon-top.js"), },{ name: "icon-traffic-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-traffic-alt.js"), },{ name: "icon-trafic", - path: () => import("./icons/icon-trafic.js"), },{ name: "icon-train", - path: () => import("./icons/icon-train.js"), },{ name: "icon-trash-alt-2", legacy: true, +hidden: true, path: () => import("./icons/icon-trash-alt-2.js"), },{ name: "icon-trash-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-trash-alt.js"), },{ name: "icon-trash", - path: () => import("./icons/icon-trash.js"), },{ name: "icon-tree", - path: () => import("./icons/icon-tree.js"), },{ name: "icon-trophy", - path: () => import("./icons/icon-trophy.js"), },{ name: "icon-truck", - path: () => import("./icons/icon-truck.js"), },{ name: "icon-tv-old", - path: () => import("./icons/icon-tv-old.js"), },{ name: "icon-tv", - path: () => import("./icons/icon-tv.js"), },{ name: "icon-umb-content", legacy: true, +hidden: true, path: () => import("./icons/icon-umb-content.js"), },{ name: "icon-umb-developer", legacy: true, +hidden: true, path: () => import("./icons/icon-umb-developer.js"), },{ name: "icon-umb-media", legacy: true, +hidden: true, path: () => import("./icons/icon-umb-media.js"), },{ name: "icon-umb-settings", legacy: true, +hidden: true, path: () => import("./icons/icon-umb-settings.js"), },{ name: "icon-umb-users", legacy: true, +hidden: true, path: () => import("./icons/icon-umb-users.js"), },{ name: "icon-umbrella", - path: () => import("./icons/icon-umbrella.js"), },{ name: "icon-undo", - path: () => import("./icons/icon-undo.js"), },{ name: "icon-underline", - path: () => import("./icons/icon-underline.js"), },{ name: "icon-unlink", - path: () => import("./icons/icon-unlink.js"), },{ name: "icon-unlocked", - path: () => import("./icons/icon-unlocked.js"), },{ name: "icon-unplug", - path: () => import("./icons/icon-unplug.js"), },{ name: "icon-untitled", legacy: true, +hidden: true, path: () => import("./icons/icon-untitled.js"), },{ name: "icon-usb-connector", legacy: true, +hidden: true, path: () => import("./icons/icon-usb-connector.js"), },{ name: "icon-usb", - path: () => import("./icons/icon-usb.js"), },{ name: "icon-user-female", legacy: true, +hidden: true, path: () => import("./icons/icon-user-female.js"), },{ name: "icon-user-females-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-user-females-alt.js"), },{ name: "icon-user-females", legacy: true, +hidden: true, path: () => import("./icons/icon-user-females.js"), },{ name: "icon-user-glasses", legacy: true, +hidden: true, path: () => import("./icons/icon-user-glasses.js"), },{ name: "icon-user", - path: () => import("./icons/icon-user.js"), },{ name: "icon-users-alt", legacy: true, +hidden: true, path: () => import("./icons/icon-users-alt.js"), },{ name: "icon-users", - path: () => import("./icons/icon-users.js"), },{ name: "icon-utilities", - path: () => import("./icons/icon-utilities.js"), },{ name: "icon-vcard", - path: () => import("./icons/icon-vcard.js"), },{ name: "icon-video", - path: () => import("./icons/icon-video.js"), },{ name: "icon-voice", - path: () => import("./icons/icon-voice.js"), },{ name: "icon-wall-plug", - path: () => import("./icons/icon-wall-plug.js"), },{ name: "icon-wallet", - path: () => import("./icons/icon-wallet.js"), },{ name: "icon-wand", - path: () => import("./icons/icon-wand.js"), },{ name: "icon-webhook", - path: () => import("./icons/icon-webhook.js"), },{ name: "icon-weight", - path: () => import("./icons/icon-weight.js"), },{ name: "icon-width", - path: () => import("./icons/icon-width.js"), },{ name: "icon-wifi", - path: () => import("./icons/icon-wifi.js"), },{ name: "icon-window-popin", - path: () => import("./icons/icon-window-popin.js"), },{ name: "icon-window-popout", - path: () => import("./icons/icon-window-popout.js"), },{ name: "icon-window-sizes", - path: () => import("./icons/icon-window-sizes.js"), },{ name: "icon-wine-glass", - path: () => import("./icons/icon-wine-glass.js"), },{ name: "icon-wrench", - path: () => import("./icons/icon-wrench.js"), },{ name: "icon-wrong", - path: () => import("./icons/icon-wrong.js"), },{ name: "icon-zip", - path: () => import("./icons/icon-zip.js"), },{ name: "icon-zom-out", legacy: true, +hidden: true, path: () => import("./icons/icon-zom-out.js"), },{ name: "icon-zoom-in", - path: () => import("./icons/icon-zoom-in.js"), },{ name: "icon-zoom-out", - path: () => import("./icons/icon-zoom-out.js"), },{ name: "icon-star", - path: () => import("./icons/icon-star.js"), },{ name: "icon-database", - path: () => import("./icons/icon-database.js"), },{ name: "icon-umb-manifest", - +hidden: true, path: () => import("./icons/icon-umb-manifest.js"), },{ +name: "icon-puzzle-piece", +path: () => import("./icons/icon-puzzle-piece.js"), +},{ name: "icon-document-3d", - path: () => import("./icons/icon-document-3d.js"), },{ name: "icon-document-medal", - path: () => import("./icons/icon-document-medal.js"), },{ name: "icon-document-chart-bar", - path: () => import("./icons/icon-document-chart-bar.js"), },{ name: "icon-document-chart-graph", - path: () => import("./icons/icon-document-chart-graph.js"), },{ name: "icon-document-html", - path: () => import("./icons/icon-document-html.js"), },{ name: "icon-document-js", - path: () => import("./icons/icon-document-js.js"), },{ name: "icon-document-key", - path: () => import("./icons/icon-document-key.js"), },{ name: "icon-document-search", - path: () => import("./icons/icon-document-search.js"), },{ name: "icon-document-settings", - path: () => import("./icons/icon-document-settings.js"), },{ name: "icon-document-spreadsheet", - path: () => import("./icons/icon-document-spreadsheet.js"), },{ name: "icon-document-command", - path: () => import("./icons/icon-document-command.js"), },{ name: "icon-document-command", - path: () => import("./icons/icon-document-command.js"), },{ name: "icon-document-font", - path: () => import("./icons/icon-document-font.js"), },{ name: "icon-document-user", - path: () => import("./icons/icon-document-user.js"), },{ name: "icon-document-image", - path: () => import("./icons/icon-document-image.js"), },{ name: "icon-document-play", - path: () => import("./icons/icon-document-play.js"), },{ name: "icon-document-play", - path: () => import("./icons/icon-document-play.js"), },{ name: "icon-facebook", - path: () => import("./icons/icon-facebook.js"), },{ name: "icon-gitbook", - path: () => import("./icons/icon-gitbook.js"), },{ name: "icon-github", - path: () => import("./icons/icon-github.js"), },{ name: "icon-gitlab", - path: () => import("./icons/icon-gitlab.js"), },{ name: "icon-google", - path: () => import("./icons/icon-google.js"), },{ name: "icon-mastodon", - path: () => import("./icons/icon-mastodon.js"), },{ name: "icon-twitter-x", - path: () => import("./icons/icon-twitter-x.js"), },{ name: "icon-art-easel", @@ -2540,146 +2223,180 @@ legacy: true, path: () => import("./icons/icon-molecular.js"), },{ name: "icon-umbraco", - path: () => import("./icons/icon-umbraco.js"), },{ name: "icon-azure", legacy: true, +hidden: true, path: () => import("./icons/icon-azure.js"), },{ name: "icon-microsoft", legacy: true, +hidden: true, path: () => import("./icons/icon-microsoft.js"), },{ name: "icon-os-x", legacy: true, +hidden: true, path: () => import("./icons/icon-os-x.js"), },{ name: "icon-pants", legacy: true, +hidden: true, path: () => import("./icons/icon-pants.js"), },{ name: "icon-parachute-drop", legacy: true, +hidden: true, path: () => import("./icons/icon-parachute-drop.js"), },{ name: "icon-parental-control", legacy: true, +hidden: true, path: () => import("./icons/icon-parental-control.js"), },{ name: "icon-path", legacy: true, +hidden: true, path: () => import("./icons/icon-path.js"), },{ name: "icon-piracy", legacy: true, +hidden: true, path: () => import("./icons/icon-piracy.js"), },{ name: "icon-poker-chip", legacy: true, +hidden: true, path: () => import("./icons/icon-poker-chip.js"), },{ name: "icon-pound-bag", legacy: true, +hidden: true, path: () => import("./icons/icon-pound-bag.js"), },{ name: "icon-receipt-dollar", legacy: true, +hidden: true, path: () => import("./icons/icon-receipt-dollar.js"), },{ name: "icon-receipt-euro", legacy: true, +hidden: true, path: () => import("./icons/icon-receipt-euro.js"), },{ name: "icon-receipt-pound", legacy: true, +hidden: true, path: () => import("./icons/icon-receipt-pound.js"), },{ name: "icon-receipt-yen", legacy: true, +hidden: true, path: () => import("./icons/icon-receipt-yen.js"), },{ name: "icon-road", legacy: true, +hidden: true, path: () => import("./icons/icon-road.js"), },{ name: "icon-safe", legacy: true, +hidden: true, path: () => import("./icons/icon-safe.js"), },{ name: "icon-safedial", legacy: true, +hidden: true, path: () => import("./icons/icon-safedial.js"), },{ name: "icon-sandbox-toys", legacy: true, +hidden: true, path: () => import("./icons/icon-sandbox-toys.js"), },{ name: "icon-security-camera", legacy: true, +hidden: true, path: () => import("./icons/icon-security-camera.js"), },{ name: "icon-settings-alt-2", legacy: true, +hidden: true, path: () => import("./icons/icon-settings-alt-2.js"), },{ name: "icon-share-alt-2", legacy: true, +hidden: true, path: () => import("./icons/icon-share-alt-2.js"), },{ name: "icon-shorts", legacy: true, +hidden: true, path: () => import("./icons/icon-shorts.js"), },{ name: "icon-simcard", legacy: true, +hidden: true, path: () => import("./icons/icon-simcard.js"), },{ name: "icon-tab", legacy: true, +hidden: true, path: () => import("./icons/icon-tab.js"), },{ name: "icon-tactics", legacy: true, +hidden: true, path: () => import("./icons/icon-tactics.js"), },{ name: "icon-theif", legacy: true, +hidden: true, path: () => import("./icons/icon-theif.js"), },{ name: "icon-thought-bubble", legacy: true, +hidden: true, path: () => import("./icons/icon-thought-bubble.js"), },{ name: "icon-twitter", legacy: true, +hidden: true, path: () => import("./icons/icon-twitter.js"), },{ name: "icon-umb-contour", legacy: true, +hidden: true, path: () => import("./icons/icon-umb-contour.js"), },{ name: "icon-umb-deploy", legacy: true, +hidden: true, path: () => import("./icons/icon-umb-deploy.js"), },{ name: "icon-umb-members", legacy: true, +hidden: true, path: () => import("./icons/icon-umb-members.js"), },{ name: "icon-universal", legacy: true, +hidden: true, path: () => import("./icons/icon-universal.js"), },{ name: "icon-war", legacy: true, +hidden: true, path: () => import("./icons/icon-war.js"), },{ name: "icon-windows", legacy: true, +hidden: true, path: () => import("./icons/icon-windows.js"), },{ name: "icon-yen-bag", legacy: true, +hidden: true, path: () => import("./icons/icon-yen-bag.js"), }]; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-clipboard-copy.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-clipboard-copy.ts index c32a59159a..b32914b26e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-clipboard-copy.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-clipboard-copy.ts @@ -1,4 +1,4 @@ -export default ` +export default ` +export default ` +export default ` +export default ` + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/types.ts index ffc7c9d5db..e8f153daa3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/types.ts @@ -5,7 +5,11 @@ export type * from './extensions/icons.extension.js'; export interface UmbIconDefinition { name: string; path: JsLoaderProperty; + /** + * @deprecated `legacy` is deprecated and will be removed in v.17. Use `hidden` instead. + */ legacy?: boolean; + hidden?: boolean; } export type UmbIconDictionary = Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts index 25e388e063..60f4a53b28 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts @@ -19,6 +19,7 @@ import { manifests as propertyTypeManifests } from './property-type/manifests.js import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; import { manifests as sectionManifests } from './section/manifests.js'; import { manifests as serverFileSystemManifests } from './server-file-system/manifests.js'; +import { manifests as temporaryFileManifests } from './temporary-file/manifests.js'; import { manifests as themeManifests } from './themes/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; @@ -47,6 +48,7 @@ export const manifests: Array = ...recycleBinManifests, ...sectionManifests, ...serverFileSystemManifests, + ...temporaryFileManifests, ...themeManifests, ...treeManifests, ...workspaceManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/notification/layouts/default/notification-layout-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/notification/layouts/default/notification-layout-default.element.ts index bca5465b68..85a36bdec0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/notification/layouts/default/notification-layout-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/notification/layouts/default/notification-layout-default.element.ts @@ -6,6 +6,7 @@ import { ifDefined, nothing, css, + styleMap, } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbNotificationDefaultData, UmbNotificationHandler } from '@umbraco-cms/backoffice/notification'; @@ -23,7 +24,7 @@ export class UmbNotificationLayoutDefaultElement extends LitElement { override render() { return html` -
${this.data.message}
+
${this.data.message}
${this.#renderStructuredList(this.data.structuredList)} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/notification/notification.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/notification/notification.context.ts index 5bdbf98670..790da2268d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/notification/notification.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/notification/notification.context.ts @@ -12,6 +12,7 @@ export interface UmbNotificationDefaultData { message: string; headline?: string; structuredList?: Record>; + whitespace?: 'normal' | 'pre-line' | 'pre-wrap' | 'nowrap' | 'pre'; } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts index 2997327f6a..bf9fffbb22 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts @@ -12,16 +12,16 @@ import { debounce } from '@umbraco-cms/backoffice/utils'; * @class UmbPickerSearchManager * @augments {UmbControllerBase} * @template ResultItemType - * @template QueryType + * @template SearchRequestArgsType */ export class UmbPickerSearchManager< ResultItemType extends UmbSearchResultItemModel = UmbSearchResultItemModel, - QueryType extends UmbSearchRequestArgs = UmbSearchRequestArgs, + SearchRequestArgsType extends UmbSearchRequestArgs = UmbSearchRequestArgs, > extends UmbControllerBase { #searchable = new UmbBooleanState(false); public readonly searchable = this.#searchable.asObservable(); - #query = new UmbObjectState(undefined); + #query = new UmbObjectState(undefined); public readonly query = this.#query.asObservable(); #searching = new UmbBooleanState(false); @@ -34,7 +34,7 @@ export class UmbPickerSearchManager< public readonly resultTotalItems = this.#resultTotalItems.asObservable(); #config?: UmbPickerSearchManagerConfig; - #searchProvider?: UmbSearchProvider; + #searchProvider?: UmbSearchProvider; /** * Creates an instance of UmbPickerSearchManager. @@ -122,11 +122,10 @@ export class UmbPickerSearchManager< /** * Set the search query. - * @param {QueryType} query The search query. + * @param {SearchRequestArgsType} query The search query. * @memberof UmbPickerSearchManager */ - public setQuery(query: QueryType) { - if (this.getSearchable() === false) throw new Error('Search is not enabled'); + public setQuery(query: SearchRequestArgsType) { if (!this.query) { this.clear(); return; @@ -138,19 +137,19 @@ export class UmbPickerSearchManager< /** * Get the current search query. * @memberof UmbPickerSearchManager - * @returns {QueryType | undefined} The current search query. + * @returns {SearchRequestArgsType | undefined} The current search query. */ - public getQuery(): QueryType | undefined { + public getQuery(): SearchRequestArgsType | undefined { return this.#query.getValue(); } /** * Update the current search query. - * @param {Partial} query + * @param {Partial} query * @memberof UmbPickerSearchManager */ - public updateQuery(query: Partial) { - const mergedQuery = { ...this.getQuery(), ...query } as QueryType; + public updateQuery(query: Partial) { + const mergedQuery = { ...this.getQuery(), ...query } as SearchRequestArgsType; this.#query.setValue(mergedQuery); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts index 1ead621347..d21f1dcd51 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts @@ -28,7 +28,6 @@ import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; * The Element will render a Property Editor based on the Property Editor UI alias passed to the element. * This will also render all Property Actions related to the Property Editor UI Alias. */ - @customElement('umb-property') export class UmbPropertyElement extends UmbLitElement { /** @@ -178,6 +177,7 @@ export class UmbPropertyElement extends UmbLitElement { #validationMessageBinder?: UmbBindServerValidationToFormControl; #valueObserver?: UmbObserverController; #configObserver?: UmbObserverController; + #validationMessageObserver?: UmbObserverController; #extensionsController?: UmbExtensionsApiInitializer; constructor() { @@ -293,6 +293,7 @@ export class UmbPropertyElement extends UmbLitElement { // cleanup: this.#valueObserver?.destroy(); this.#configObserver?.destroy(); + this.#validationMessageObserver?.destroy(); this.#controlValidator?.destroy(); oldElement?.removeEventListener('change', this._onPropertyEditorChange as any as EventListener); oldElement?.removeEventListener('property-value-change', this._onPropertyEditorChange as any as EventListener); @@ -330,7 +331,7 @@ export class UmbPropertyElement extends UmbLitElement { }, null, ); - this.#configObserver = this.observe( + this.#validationMessageObserver = this.observe( this.#propertyContext.validationMandatoryMessage, (mandatoryMessage) => { if (mandatoryMessage) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-source-response.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-source-response.interface.ts index 613ec25078..acfe5fb5c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-source-response.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-source-response.interface.ts @@ -8,5 +8,9 @@ export interface UmbDataSourceErrorResponse { // TODO: we should not rely on the ApiError and CancelError types from the backend-api package // We need to be able to return a generic error type that can be used in the frontend // Example: the clipboard is getting is data from local storage, so it should not use the ApiError type + /** + * The error that occurred when fetching the data. + * The {ApiError} and {CancelError} types will change in the future to be a more generic error type. + */ error?: ApiError | CancelError; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts index c8b2e5b8bd..d54166a951 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts @@ -4,6 +4,8 @@ import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { type ManifestRepository, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbEntityUpdatedEvent } from '@umbraco-cms/backoffice/entity-action'; const ObserveRepositoryAlias = Symbol(); @@ -14,6 +16,7 @@ export class UmbRepositoryItemsManager exte #init: Promise; #currentRequest?: Promise; + #eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; // the init promise is used externally for recognizing when the manager is ready. public get init() { @@ -70,14 +73,28 @@ export class UmbRepositoryItemsManager exte }, null, ); + + this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => { + this.#eventContext = context; + + this.#eventContext.removeEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); + + this.#eventContext.addEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); + }); } getUniques(): Array { return this.#uniques.getValue(); } - setUniques(uniques: string[]): void { - this.#uniques.setValue(uniques); + setUniques(uniques: string[] | undefined): void { + this.#uniques.setValue(uniques ?? []); } getItems(): Array { @@ -122,6 +139,25 @@ export class UmbRepositoryItemsManager exte } } + async #reloadItem(unique: string): Promise { + await this.#init; + if (!this.repository) throw new Error('Repository is not initialized'); + + const { data } = await this.repository.requestItems([unique]); + + if (data) { + const items = this.getItems(); + const item = items.find((item) => this.#getUnique(item) === unique); + + if (item) { + const index = items.indexOf(item); + const newItems = [...items]; + newItems[index] = data[0]; + this.#items.setValue(this.#sortByUniques(newItems)); + } + } + } + #sortByUniques(data: Array): Array { const uniques = this.getUniques(); return [...data].sort((a, b) => { @@ -130,4 +166,25 @@ export class UmbRepositoryItemsManager exte return aIndex - bIndex; }); } + + #onEntityUpdatedEvent = (event: UmbEntityUpdatedEvent) => { + const eventUnique = event.getUnique(); + + const items = this.getItems(); + if (items.length === 0) return; + + // Ignore events if the entity is not in the list of items. + const item = items.find((item) => this.#getUnique(item) === eventUnique); + if (!item) return; + + this.#reloadItem(item.unique); + }; + + override destroy(): void { + this.#eventContext?.removeEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); + super.destroy(); + } } 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 7b0f05fef4..6de800cccb 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,5 +1,7 @@ export * from './resource.controller.js'; export * from './tryExecute.function.js'; export * from './tryExecuteAndNotify.function.js'; +export * from './tryXhrRequest.function.js'; export * from './extractUmbColorVariable.function.js'; export * from './apiTypeValidators.function.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts index 59c7110b92..d92a68a38c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts @@ -1,12 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { UMB_AUTH_CONTEXT } from '../auth/index.js'; import { isApiError, isCancelError, isCancelablePromise } from './apiTypeValidators.function.js'; +import type { XhrRequestOptions } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UMB_NOTIFICATION_CONTEXT, type UmbNotificationOptions } from '@umbraco-cms/backoffice/notification'; import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; -import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api'; +import { + ApiError, + CancelablePromise, + CancelError, + type ProblemDetails, +} from '@umbraco-cms/backoffice/external/backend-api'; export class UmbResourceController extends UmbControllerBase { #promise: Promise; @@ -72,7 +78,7 @@ export class UmbResourceController extends UmbControllerBase { // Cancelled - do nothing return {}; } else { - console.group('ApiError caught in UmbResourceController'); + console.groupCollapsed('ApiError caught in UmbResourceController'); console.error('Request failed', error.request); console.error('Request body', error.body); console.error('Error', error); @@ -167,6 +173,117 @@ export class UmbResourceController extends UmbControllerBase { return { data, error }; } + /** + * Make an XHR request. + * @param host The controller host for this controller to be appended to. + * @param options The options for the XHR request. + */ + static xhrRequest(options: XhrRequestOptions): CancelablePromise { + const baseUrl = options.baseUrl || '/umbraco'; + + const promise = new CancelablePromise(async (resolve, reject, onCancel) => { + const xhr = new XMLHttpRequest(); + xhr.open(options.method, `${baseUrl}${options.url}`, true); + + // Set default headers + if (options.token) { + const token = typeof options.token === 'function' ? await options.token() : options.token; + if (token) { + xhr.setRequestHeader('Authorization', `Bearer ${token}`); + } + } + + // Infer Content-Type header based on body type + if (options.body instanceof FormData) { + // Note: 'multipart/form-data' is automatically set by the browser for FormData + } else { + xhr.setRequestHeader('Content-Type', 'application/json'); + } + + // Set custom headers + if (options.headers) { + for (const [key, value] of Object.entries(options.headers)) { + xhr.setRequestHeader(key, value); + } + } + + xhr.upload.onprogress = (event) => { + if (options.onProgress) { + options.onProgress(event); + } + }; + + xhr.onload = () => { + try { + if (xhr.status >= 200 && xhr.status < 300) { + if (options.responseHeader) { + const response = xhr.getResponseHeader(options.responseHeader); + resolve(response as T); + } else { + resolve(JSON.parse(xhr.responseText)); + } + } else { + // TODO: [JOV] This has to be changed into our own error type, when we have a chance to introduce a breaking change in the future. + const error = new ApiError( + { + method: options.method, + url: `${baseUrl}${options.url}`, + }, + { + body: xhr.responseText, + ok: false, + status: xhr.status, + statusText: xhr.statusText, + url: xhr.responseURL, + }, + xhr.statusText, + ); + reject(error); + } + } catch { + // This most likely happens when the response is not JSON + reject(new Error(`Failed to make request: ${xhr.statusText}`)); + } + }; + + xhr.onerror = () => { + // TODO: [JOV] This has to be changed into our own error type, when we have a chance to introduce a breaking change in the future. + const error = new ApiError( + { + method: options.method, + url: `${baseUrl}${options.url}`, + }, + { + body: xhr.responseText, + ok: false, + status: xhr.status, + statusText: xhr.statusText, + url: xhr.responseURL, + }, + xhr.statusText, + ); + reject(error); + }; + + if (!onCancel.isCancelled) { + // Handle body based on Content-Type + if (options.body instanceof FormData) { + xhr.send(options.body); + } else { + xhr.send(JSON.stringify(options.body)); + } + } + + onCancel(() => { + xhr.abort(); + // TODO: [JOV] This has to be changed into our own error type, when we have a chance to introduce a breaking change in the future. + reject(new CancelError('Request was cancelled.')); + }); + }); + + return promise; + } + /** * Cancel all resources that are currently being executed by this controller if they are cancelable. * 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/tryXhrRequest.function.ts new file mode 100644 index 0000000000..749fafc729 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/tryXhrRequest.function.ts @@ -0,0 +1,16 @@ +import type { XhrRequestOptions } from './types.js'; +import { UmbResourceController } from './resource.controller.js'; +import { OpenAPI, type CancelablePromise } from '@umbraco-cms/backoffice/external/backend-api'; + +/** + * Make an XHR request. + * @param {XhrRequestOptions} options The options for the XHR request. + * @returns {CancelablePromise} A promise that can be cancelled. + */ +export function tryXhrRequest(options: XhrRequestOptions): CancelablePromise { + return UmbResourceController.xhrRequest({ + ...options, + baseUrl: OpenAPI.BASE, + token: OpenAPI.TOKEN as never, + }); +} 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 new file mode 100644 index 0000000000..2ff6371c63 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts @@ -0,0 +1,10 @@ +export interface XhrRequestOptions { + baseUrl?: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; + url: string; + body?: unknown; + token?: string | (() => string | Promise); + headers?: Record; + responseHeader?: string; + onProgress?: (event: ProgressEvent) => void; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar/section-sidebar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar/section-sidebar.element.ts index 162a4d5bf8..ded4640fda 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar/section-sidebar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar/section-sidebar.element.ts @@ -37,7 +37,6 @@ export class UmbSectionSidebarElement extends UmbLitElement { flex-direction: column; z-index: 10; position: relative; - padding-bottom: var(--uui-size-4); box-sizing: border-box; } 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 b90c7c1614..ef285d0b90 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 @@ -221,7 +221,7 @@ export type UmbSorterConfig = Partial, 'ignorerSelector' | 'containerSelector' | 'identifier'>>; /** - + * @class UmbSorterController * @implements {UmbControllerInterface} * @description This controller can make user able to sort items. @@ -346,10 +346,8 @@ export class UmbSorterController): void { - if (this.#model) { - this.#model = model; - } + setModel(model: Array | undefined): void { + this.#model = model ?? []; } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/components/temporary-file-badge.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/components/temporary-file-badge.element.ts index 9577279569..5c386e7239 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/components/temporary-file-badge.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/components/temporary-file-badge.element.ts @@ -4,70 +4,72 @@ import { clamp } from '@umbraco-cms/backoffice/utils'; @customElement('umb-temporary-file-badge') export class UmbTemporaryFileBadgeElement extends UmbLitElement { - private _progress = 0; + #progress = 0; @property({ type: Number }) public set progress(v: number) { - const oldVal = this._progress; - - const p = clamp(v, 0, 100); - this._progress = p; - - this.requestUpdate('progress', oldVal); + const p = clamp(Math.ceil(v), 0, 100); + this.#progress = p; } public get progress(): number { - return this._progress; + return this.#progress; } @property({ type: Boolean, reflect: true }) public complete = false; + @property({ type: Boolean, reflect: true }) + public error = false; + override render() { - return html` -
- - ${this.complete - ? html`` - : html``} -
-
`; + return html`
+ +
${this.#renderIcon()}
+
`; } - static override styles = css` - :host { - display: block; + #renderIcon() { + if (this.error) { + return html``; } + if (this.complete) { + return html``; + } + + return `${this.progress}%`; + } + + static override readonly styles = css` #wrapper { - box-sizing: border-box; - box-shadow: inset 0px 0px 0px 6px var(--uui-color-surface); - background-color: var(--uui-color-selected); position: relative; - border-radius: 100%; - font-size: var(--uui-size-6); + height: 75%; } - :host([complete]) #wrapper { - background-color: var(--uui-color-positive); + :host([complete]) { + uui-loader-circle, + #icon { + color: var(--uui-color-positive); + } } - :host([complete]) uui-loader-circle { - color: var(--uui-color-positive); + :host([error]) { + uui-loader-circle, + #icon { + color: var(--uui-color-danger); + } } uui-loader-circle { - display: absolute; z-index: 2; inset: 0; color: var(--uui-color-focus); font-size: var(--uui-size-12); + width: 100%; + height: 100%; } - uui-badge { - padding: 0; - background-color: transparent; - } - - uui-icon { + #icon { + color: var(--uui-color-text); font-size: var(--uui-size-6); position: absolute; top: 50%; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts new file mode 100644 index 0000000000..577f31f00f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.repository.ts @@ -0,0 +1,69 @@ +import type { UmbTemporaryFileConfigurationModel } from '../types.js'; +import { UmbTemporaryFileConfigServerDataSource } from './config.server.data-source.js'; +import { UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT } from './config.store.token.js'; +import { UMB_TEMPORARY_FILE_REPOSITORY_ALIAS } from './constants.js'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { Observable } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbTemporaryFileConfigRepository extends UmbRepositoryBase implements UmbApi { + /** + * Promise that resolves when the repository has been initialized, i.e. when the configuration has been fetched from the server. + */ + initialized: Promise; + + #dataStore?: typeof UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT.TYPE; + #dataSource = new UmbTemporaryFileConfigServerDataSource(this); + + constructor(host: UmbControllerHost) { + super(host, UMB_TEMPORARY_FILE_REPOSITORY_ALIAS.toString()); + this.initialized = new Promise((resolve) => { + this.consumeContext(UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT, async (store) => { + this.#dataStore = store; + await this.#init(); + resolve(); + }); + }); + } + + async #init() { + // Check if the store already has data + if (this.#dataStore?.getState()) { + return; + } + + const { data } = await this.#dataSource.getConfig(); + + if (data) { + this.#dataStore?.update(data); + } + } + + /** + * Subscribe to the entire configuration. + */ + all() { + if (!this.#dataStore) { + throw new Error('Data store not initialized'); + } + + return this.#dataStore.all(); + } + + /** + * Subscribe to a part of the configuration. + * @param part + */ + part( + part: Part, + ): Observable { + if (!this.#dataStore) { + throw new Error('Data store not initialized'); + } + + return this.#dataStore.part(part); + } +} + +export default UmbTemporaryFileConfigRepository; 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 new file mode 100644 index 0000000000..10d2db7a04 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.server.data-source.ts @@ -0,0 +1,18 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { TemporaryFileService } from '@umbraco-cms/backoffice/external/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +export class UmbTemporaryFileConfigServerDataSource { + #host; + + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Get the temporary file configuration. + */ + getConfig() { + return tryExecuteAndNotify(this.#host, TemporaryFileService.getTemporaryFileConfiguration()); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.token.ts new file mode 100644 index 0000000000..04155b61d7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.token.ts @@ -0,0 +1,7 @@ +import type { UmbTemporaryFileConfigStore } from './config.store.js'; +import { UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS } from './constants.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT = new UmbContextToken( + UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.ts new file mode 100644 index 0000000000..18efd1d3c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/config.store.ts @@ -0,0 +1,12 @@ +import type { UmbTemporaryFileConfigurationModel } from '../types.js'; +import { UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT } from './config.store.token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbStoreObjectBase } from '@umbraco-cms/backoffice/store'; + +export class UmbTemporaryFileConfigStore extends UmbStoreObjectBase { + constructor(host: UmbControllerHost) { + super(host, UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT.toString()); + } +} + +export default UmbTemporaryFileConfigStore; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/constants.ts new file mode 100644 index 0000000000..6f4a206bc0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/constants.ts @@ -0,0 +1,2 @@ +export const UMB_TEMPORARY_FILE_REPOSITORY_ALIAS = 'Umb.Repository.TemporaryFile.Config'; +export const UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS = 'UmbTemporaryFileConfigStore'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/index.ts new file mode 100644 index 0000000000..3b4d98c12e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/index.ts @@ -0,0 +1,4 @@ +export * from './config.repository.js'; +export * from './config.store.token.js'; +export * from './config.store.js'; +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/manifests.ts new file mode 100644 index 0000000000..9b5af26e65 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/config/manifests.ts @@ -0,0 +1,16 @@ +import { UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS, UMB_TEMPORARY_FILE_REPOSITORY_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'store', + alias: UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS, + name: 'Temporary File Config Store', + api: () => import('./config.store.js'), + }, + { + type: 'repository', + alias: UMB_TEMPORARY_FILE_REPOSITORY_ALIAS, + name: 'Temporary File Config Repository', + api: () => import('./config.repository.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/index.ts index 0a663e6dd2..888e6d64b0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/index.ts @@ -1,4 +1,5 @@ export * from './temporary-file.repository.js'; export * from './components/temporary-file-badge.element.js'; +export * from './config/index.js'; export * from './temporary-file-manager.class.js'; export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/manifests.ts new file mode 100644 index 0000000000..ec7e1c29c5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as configManifests } from './config/manifests.js'; + +export const manifests = [...configManifests]; 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 8e52de540b..52b14b48c3 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 @@ -1,17 +1,23 @@ import { UmbTemporaryFileRepository } from './temporary-file.repository.js'; +import { UmbTemporaryFileConfigRepository } from './config/index.js'; import { TemporaryFileStatus, type UmbQueueHandlerCallback, type UmbTemporaryFileModel, type UmbUploadOptions, } from './types.js'; -import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { observeMultiple, UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { formatBytes } from '@umbraco-cms/backoffice/utils'; export class UmbTemporaryFileManager< UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel, > extends UmbControllerBase { readonly #temporaryFileRepository = new UmbTemporaryFileRepository(this._host); + readonly #temporaryFileConfigRepository = new UmbTemporaryFileConfigRepository(this._host); + readonly #localization = new UmbLocalizationController(this._host); readonly #queue = new UmbArrayState([], (item) => item.temporaryUnique); public readonly queue = this.#queue.asObservable(); @@ -20,8 +26,8 @@ export class UmbTemporaryFileManager< this.#queue.setValue([]); const item: UploadableItem = { - status: TemporaryFileStatus.WAITING, ...uploadableItem, + status: TemporaryFileStatus.WAITING, }; this.#queue.appendOne(item); @@ -34,7 +40,7 @@ export class UmbTemporaryFileManager< ): Promise> { this.#queue.setValue([]); - const items = queueItems.map((item): UploadableItem => ({ status: TemporaryFileStatus.WAITING, ...item })); + const items = queueItems.map((item): UploadableItem => ({ ...item, status: TemporaryFileStatus.WAITING })); this.#queue.append(items); return this.#handleQueue({ ...options }); } @@ -71,22 +77,69 @@ export class UmbTemporaryFileManager< return filesCompleted; } + async #validateItem(item: UploadableItem): Promise { + let maxFileSize = await this.observe(this.#temporaryFileConfigRepository.part('maxFileSize')).asPromise(); + if (maxFileSize) { + // Convert from kilobytes to bytes + maxFileSize *= 1024; + if (item.file.size > maxFileSize) { + const notification = await this.getContext(UMB_NOTIFICATION_CONTEXT); + notification.peek('warning', { + data: { + headline: 'Upload', + message: ` + ${this.#localization.term('media_invalidFileSize')}: ${item.file.name} (${formatBytes(item.file.size)}). + + ${this.#localization.term('media_maxFileSize')} ${formatBytes(maxFileSize)}. + `, + whitespace: 'pre-line', + }, + }); + return false; + } + } + + const fileExtension = item.file.name.split('.').pop() ?? ''; + + const [allowedExtensions, disallowedExtensions] = await this.observe( + observeMultiple([ + this.#temporaryFileConfigRepository.part('allowedUploadedFileExtensions'), + this.#temporaryFileConfigRepository.part('disallowedUploadedFilesExtensions'), + ]), + ).asPromise(); + + if ( + (allowedExtensions?.length && !allowedExtensions.includes(fileExtension)) || + (disallowedExtensions?.length && disallowedExtensions.includes(fileExtension)) + ) { + const notification = await this.getContext(UMB_NOTIFICATION_CONTEXT); + notification.peek('warning', { + data: { + message: `${this.#localization.term('media_disallowedFileType')}: ${fileExtension}`, + }, + }); + return false; + } + + return true; + } + async #handleUpload(item: UploadableItem) { if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`); - const { error } = await this.#temporaryFileRepository - .upload(item.temporaryUnique, item.file) - .catch(() => ({ error: true })); - - let status: TemporaryFileStatus; - if (error) { - status = TemporaryFileStatus.ERROR; - this.#queue.updateOne(item.temporaryUnique, { ...item, status }); - } else { - status = TemporaryFileStatus.SUCCESS; - this.#queue.updateOne(item.temporaryUnique, { ...item, status }); + const isValid = await this.#validateItem(item); + if (!isValid) { + this.#queue.updateOne(item.temporaryUnique, { ...item, status: TemporaryFileStatus.ERROR }); + return { ...item, status: TemporaryFileStatus.ERROR }; } + const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, item.file, (evt) => { + // Update progress in percent if a callback is provided + if (item.onProgress) item.onProgress((evt.loaded / evt.total) * 100); + }); + const status = error ? TemporaryFileStatus.ERROR : TemporaryFileStatus.SUCCESS; + + this.#queue.updateOne(item.temporaryUnique, { ...item, status }); return { ...item, status }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.repository.ts index 8233b80fd9..519dc77652 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.repository.ts @@ -27,8 +27,8 @@ export class UmbTemporaryFileRepository extends UmbRepositoryBase { * @returns {*} * @memberof UmbTemporaryFileRepository */ - upload(id: string, file: File) { - return this.#source.create(id, file); + upload(id: string, file: File, onProgress?: (progress: ProgressEvent) => void) { + return this.#source.create(id, file, onProgress); } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.server.data-source.ts index 9e4d11d55a..644eaa7dcd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file.server.data-source.ts @@ -1,6 +1,7 @@ -import { TemporaryFileService } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbDataSourceResponse } from '../repository/index.js'; +import { TemporaryFileService, type PostTemporaryFileResponse } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { tryExecuteAndNotify, tryXhrRequest } from '@umbraco-cms/backoffice/resources'; /** * A data source to upload temporary files to the server @@ -26,16 +27,22 @@ export class UmbTemporaryFileServerDataSource { * @returns {*} * @memberof UmbTemporaryFileServerDataSource */ - async create(id: string, file: File) { - return tryExecuteAndNotify( - this.#host, - TemporaryFileService.postTemporaryFile({ - formData: { - Id: id, - File: file, - }, - }), - ); + async create( + id: string, + file: File, + onProgress?: (progress: ProgressEvent) => void, + ): Promise> { + const body = new FormData(); + body.append('Id', id); + body.append('File', file); + const xhrRequest = tryXhrRequest({ + url: '/umbraco/management/api/v1/temporary-file', + method: 'POST', + responseHeader: 'Umb-Generated-Resource', + body, + onProgress, + }); + return tryExecuteAndNotify(this.#host, xhrRequest); } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts index 2c28850f47..9d417c4bf4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts @@ -1,3 +1,5 @@ +import type { TemporaryFileConfigurationResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + export enum TemporaryFileStatus { SUCCESS = 'success', WAITING = 'waiting', @@ -8,6 +10,7 @@ export interface UmbTemporaryFileModel { file: File; temporaryUnique: string; status?: TemporaryFileStatus; + onProgress?: (progress: number) => void; } export type UmbQueueHandlerCallback = (item: TItem) => Promise; @@ -16,3 +19,5 @@ export type UmbUploadOptions = { chunkSize?: number; callback?: UmbQueueHandlerCallback; }; + +export type UmbTemporaryFileConfigurationModel = TemporaryFileConfigurationResponseModel; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/sort-children-of/modal/sort-children-of-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/sort-children-of/modal/sort-children-of-modal.element.ts index 9fa721f2c1..981c811a08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/sort-children-of/modal/sort-children-of-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/sort-children-of/modal/sort-children-of-modal.element.ts @@ -287,8 +287,9 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement< } #renderChild(item: UmbTreeItemModel) { - return html` - + // TODO: find a way to get the icon for the item. We do not have the icon in the tree item model. + return html` + ${item.name} ${this.#renderCreateDate(item)} `; @@ -310,6 +311,14 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement< width: 100%; } + uui-table-cell { + padding: var(--uui-size-space-2) var(--uui-size-space-5); + } + + uui-table-head-cell { + padding: 0 var(--uui-size-space-5); + } + uui-table-head-cell button { background-color: transparent; color: inherit; @@ -328,9 +337,17 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement< visibility: hidden; } + uui-table-row[id='content-node']:hover { + cursor: grab; + } + uui-icon[name='icon-navigation'] { cursor: hand; } + + uui-box { + --uui-box-default-padding: 0; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts index 9f775bf189..a7b47cd0b6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts @@ -55,7 +55,17 @@ export class UmbTreePickerModalElement + ${this.#renderSearch()} ${this.#renderTree()} ${this.#renderActions()} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts new file mode 100644 index 0000000000..3664bcafeb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.function.ts @@ -0,0 +1,45 @@ +/* This Source Code has been derived from Lee Kelleher's Contentment. + * https://github.com/leekelleher/umbraco-contentment/blob/develop/src/Umbraco.Community.Contentment/DataEditors/Bytes/bytes.js + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2019 Lee Kelleher. + * Modifications are licensed under the MIT License. + */ + +export interface IFormatBytesOptions { + /** + * Number of kilobytes, default is 1024. + * @example 1000 (1KB) or 1024 (1KiB) + */ + kilo?: number; + + /** + * Number of decimal places, default is 2. + * @example 0, 1, 2, 3, etc. + */ + decimals?: number; + + /** + * The culture to use for formatting the number itself, default is `undefined` which means the browser's default culture. + * @example 'en-GB', 'en-US', 'fr-FR', etc. + */ + culture?: string; +} + +/** + * Format bytes as human-readable text. + * @param {number} bytes - The number of bytes to format. + * @param {IFormatBytesOptions} opts - Optional settings. + * @returns {string} - The formatted bytes. + */ +export function formatBytes(bytes: number, opts?: IFormatBytesOptions): string { + if (bytes === 0) return '0 Bytes'; + + const k = opts?.kilo ?? 1024; + const dm = opts?.decimals ?? 2; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + const n = parseFloat((bytes / Math.pow(k, i)).toFixed(dm)); + + return `${n.toLocaleString(opts?.culture)} ${sizes[i]}`; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts new file mode 100644 index 0000000000..2177b72216 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/bytes/bytes.test.ts @@ -0,0 +1,28 @@ +import { expect } from '@open-wc/testing'; +import { formatBytes } from './bytes.function.js'; + +describe('bytes', () => { + it('should format bytes as human-readable text', () => { + expect(formatBytes(0)).to.equal('0 Bytes'); + expect(formatBytes(1024)).to.equal('1 KB'); + expect(formatBytes(1024 * 1024)).to.equal('1 MB'); + expect(formatBytes(1024 * 1024 * 1024)).to.equal('1 GB'); + expect(formatBytes(1024 * 1024 * 1024 * 1024)).to.equal('1 TB'); + }); + + it('should format bytes as human-readable text with decimal places', () => { + expect(formatBytes(1587.2, { decimals: 0 })).to.equal('2 KB'); + expect(formatBytes(1587.2, { decimals: 1 })).to.equal('1.6 KB'); + }); + + it('should format bytes as human-readable text with different kilobytes', () => { + expect(formatBytes(1000, { kilo: 1000 })).to.equal('1 KB'); + expect(formatBytes(1000 * 1000, { kilo: 1000 })).to.equal('1 MB'); + expect(formatBytes(1000 * 1000 * 1000, { kilo: 1000 })).to.equal('1 GB'); + expect(formatBytes(1000 * 1000 * 1000 * 1000, { kilo: 1000 })).to.equal('1 TB'); + }); + + it('should format bytes as human-readable text with different culture', () => { + expect(formatBytes(1587.2, { decimals: 1, culture: 'da-DK' })).to.equal('1,6 KB'); + }); +}); 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 99441ca996..e6aabbc8d1 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 './bytes/bytes.function.js'; export * from './debounce/debounce.function.js'; export * from './direction/index.js'; export * from './download/blob-download.function.js'; @@ -19,6 +20,7 @@ export * from './path/stored-path.function.js'; export * from './path/transform-server-path-to-client-path.function.js'; export * from './path/umbraco-path.function.js'; export * from './path/url-pattern-to-string.function.js'; +export * from './sanitize/escape-html.function.js'; export * from './sanitize/sanitize-html.function.js'; export * from './selection-manager/selection.manager.js'; export * from './state-manager/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.test.ts index 8fc9ef5ece..409f492bf2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.test.ts @@ -1,6 +1,5 @@ -import { retrieveStoredPath, setStoredPath } from './stored-path.function.js'; +import { retrieveStoredPath, setStoredPath, UMB_STORAGE_REDIRECT_URL } from './stored-path.function.js'; import { expect } from '@open-wc/testing'; -import { UMB_STORAGE_REDIRECT_URL } from '@umbraco-cms/backoffice/auth'; describe('retrieveStoredPath', () => { beforeEach(() => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.ts index 01e877a5bb..e9196ab324 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/stored-path.function.ts @@ -1,5 +1,6 @@ import { ensureLocalPath } from './ensure-local-path.function.js'; -import { UMB_STORAGE_REDIRECT_URL } from '@umbraco-cms/backoffice/auth'; + +export const UMB_STORAGE_REDIRECT_URL = 'umb:auth:redirect'; /** * Retrieve the stored path from the session storage. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/escape-html.function.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/escape-html.function.test.ts new file mode 100644 index 0000000000..24e8cf5014 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/escape-html.function.test.ts @@ -0,0 +1,8 @@ +import { expect } from '@open-wc/testing'; +import { escapeHTML } from './escape-html.function.js'; + +describe('escapeHtml', () => { + it('should escape html', () => { + expect(escapeHTML('')).to.equal('<script>alert("XSS")</script>'); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/escape-html.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/escape-html.function.ts new file mode 100644 index 0000000000..ee84b1ee86 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/escape-html.function.ts @@ -0,0 +1,29 @@ +const SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; +// Match everything outside of normal chars and " (quote character) +const NON_ALPHANUMERIC_REGEXP = /([^#-~| |!])/g; + +/** + * Escapes HTML entities in a string. + * @example escapeHTML(''), // "<script>alert("XSS")</script>" + * @param html The HTML string to escape. + * @returns The sanitized HTML string. + */ +export function escapeHTML(html: unknown): string { + if (typeof html !== 'string' && html instanceof String === false) { + return html as string; + } + + return html + .toString() + .replace(/&/g, '&') + .replace(SURROGATE_PAIR_REGEXP, function (value) { + const hi = value.charCodeAt(0); + const low = value.charCodeAt(1); + return '&#' + ((hi - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000) + ';'; + }) + .replace(NON_ALPHANUMERIC_REGEXP, function (value) { + return '&#' + value.charCodeAt(0) + ';'; + }) + .replace(//g, '>'); +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/sanitize-html.function.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/sanitize-html.function.test.ts new file mode 100644 index 0000000000..05daf44862 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/sanitize/sanitize-html.function.test.ts @@ -0,0 +1,24 @@ +import { expect } from '@open-wc/testing'; +import { sanitizeHTML } from './sanitize-html.function.js'; + +describe('sanitizeHTML', () => { + it('should allow benign HTML', () => { + expect(sanitizeHTML('Test')).to.equal('Test'); + }); + + it('should remove potentially harmful content', () => { + expect(sanitizeHTML('')).to.equal(''); + }); + + it('should remove potentially harmful attributes', () => { + expect(sanitizeHTML('Test')).to.equal('Test'); + }); + + it('should remove potentially harmful content and attributes', () => { + expect(sanitizeHTML('')).to.equal(''); + }); + + it('should allow benign attributes', () => { + expect(sanitizeHTML('Test')).to.equal('Test'); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validator.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validator.context.ts index fc726492af..f856d42a55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validator.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validator.context.ts @@ -24,6 +24,7 @@ export class UmbServerModelValidatorContext { #validatePromise?: Promise; #validatePromiseResolve?: () => void; + #validatePromiseReject?: () => void; #context?: typeof UMB_VALIDATION_CONTEXT.TYPE; #isValid = true; @@ -50,9 +51,10 @@ export class UmbServerModelValidatorContext this.#context?.messages.removeMessagesByType('server'); this.#isValid = false; - //this.#validatePromiseReject?.(); - this.#validatePromise = new Promise((resolve) => { + this.#validatePromiseReject?.(); + this.#validatePromise = new Promise((resolve, reject) => { this.#validatePromiseResolve = resolve; + this.#validatePromiseReject = reject; }); // Store this state of the data for translator look ups: @@ -100,6 +102,7 @@ export class UmbServerModelValidatorContext this.#validatePromiseResolve?.(); this.#validatePromiseResolve = undefined; + this.#validatePromiseReject = undefined; } get isValid(): boolean { @@ -112,7 +115,12 @@ export class UmbServerModelValidatorContext return this.#isValid ? Promise.resolve() : Promise.reject(); } - reset(): void {} + reset(): void { + this.#isValid = true; + this.#validatePromiseReject?.(); + this.#validatePromiseResolve = undefined; + this.#validatePromiseReject = undefined; + } focusFirstInvalidElement(): void {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.test.ts new file mode 100644 index 0000000000..59d237cf2b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.test.ts @@ -0,0 +1,34 @@ +import { expect } from '@open-wc/testing'; +import { UmbValidationMessagesManager } from './validation-messages.manager'; +import { UmbObserver } from '@umbraco-cms/backoffice/observable-api'; + +describe('UmbValidationMessagesManager', () => { + let messages: UmbValidationMessagesManager; + + beforeEach(() => { + messages = new UmbValidationMessagesManager(); + }); + + it('knows if it has any messages', () => { + messages.addMessage('server', '$.test', 'test'); + + expect(messages.getHasAnyMessages()).to.be.true; + }); + + it('knows if it has any messages of a certain path or descending path', () => { + messages.addMessage('server', `$.values[?(@.id == '123')].value`, 'test'); + + expect(messages.getHasMessagesOfPathAndDescendant(`$.values[?(@.id == '123')]`)).to.be.true; + }); + + it('enables you to observe for path or descending path messages', async () => { + messages.addMessage('server', `$.values[?(@.id == '123')].value`, 'test'); + + const observeable = messages.hasMessagesOfPathAndDescendant(`$.values[?(@.id == '123')]`); + + const observer = new UmbObserver(observeable); + const result = await observer.asPromise(); + + expect(result).to.be.true; + }); +}); 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 8e7cbd3635..49715f1fe6 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 @@ -192,6 +192,9 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal */ addValidator(validator: UmbValidator): void { if (this.#validators.includes(validator)) return; + if (validator === this) { + throw new Error('Cannot add it self as validator'); + } this.#validators.push(validator); //validator.addEventListener('change', this.#onValidatorChange); if (this.#validationMode) { @@ -231,7 +234,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal if (!this.messages) { // This Context has been destroyed while is was validating, so we should not continue. - return; + return Promise.reject(); } // If we have any messages then we are not valid, otherwise lets check the validation results: [NL] @@ -264,6 +267,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal */ reset(): void { this.#validationMode = false; + this.messages.clear(); this.#validators.forEach((v) => v.reset()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts index 148a9bd599..f4f8438daf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts @@ -168,7 +168,6 @@ export function UmbFormControlMixin< /*if (e.composedPath().some((x) => x === this)) { return; }*/ - this.pristine = false; this.checkValidity(); }); } 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 f2845b31bc..106d01fcb7 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 @@ -1,34 +1,18 @@ import type { ActiveVariant } from '../../controllers/index.js'; import { UMB_WORKSPACE_SPLIT_VIEW_CONTEXT } from './workspace-split-view.context.js'; -import { - type UUIInputElement, - UUIInputEvent, - type UUIPopoverContainerElement, -} from '@umbraco-cms/backoffice/external/uui'; -import { - css, - html, - nothing, - customElement, - state, - query, - ifDefined, - type TemplateResult, -} from '@umbraco-cms/backoffice/external/lit'; -import { - UmbVariantId, - type UmbEntityVariantModel, - type UmbEntityVariantOptionModel, -} from '@umbraco-cms/backoffice/variant'; -import { UMB_PROPERTY_DATASET_CONTEXT, isNameablePropertyDatasetContext } from '@umbraco-cms/backoffice/property'; -import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element'; +import { css, customElement, html, ifDefined, nothing, query, ref, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UmbVariantState } from '@umbraco-cms/backoffice/utils'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbDataPathVariantQuery, umbBindToValidation } from '@umbraco-cms/backoffice/validation'; +import { UMB_PROPERTY_DATASET_CONTEXT, isNameablePropertyDatasetContext } from '@umbraco-cms/backoffice/property'; +import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import type { UmbContentWorkspaceContext } from '@umbraco-cms/backoffice/content'; +import type { UmbEntityVariantModel, UmbEntityVariantOptionModel } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantState } from '@umbraco-cms/backoffice/utils'; +import type { UUIInputElement, UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui'; -const elementName = 'umb-workspace-split-view-variant-selector'; -@customElement(elementName) +@customElement('umb-workspace-split-view-variant-selector') export class UmbWorkspaceSplitViewVariantSelectorElement< VariantOptionModelType extends UmbEntityVariantOptionModel = UmbEntityVariantOptionModel, @@ -64,7 +48,7 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< private _variantSelectorOpen = false; @state() - private _readOnlyCultures: string[] = []; + private _readOnlyCultures: Array = []; // eslint-disable-next-line @typescript-eslint/no-unused-vars protected _variantSorter = (a: VariantOptionModelType, b: VariantOptionModelType) => { @@ -197,8 +181,7 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< #setReadOnlyCultures() { this._readOnlyCultures = this._variantOptions .filter((variant) => this._readOnlyStates.some((state) => state.variantId.compare(variant))) - .map((variant) => variant.culture) - .filter((item) => item !== null) as string[]; + .map((variant) => variant.culture); } #onPopoverToggle(event: ToggleEvent) { @@ -220,66 +203,74 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< this._popoverElement.style.width = `${host.width}px`; } + /** + * Focuses the input element after a short delay to ensure it is rendered. + * This works better than the {umbFocus()} directive, which does not work in this context. + */ + #focusInput(element?: Element) { + if (!element) return; + + setTimeout(async () => { + await this.updateComplete; + (element as UUIInputElement)?.focus(); + }, 200); + } + override render() { - return this._variantId - ? html` + if (!this._variantId) return nothing; + + return html` - ${ - this.#hasVariants() - ? html` - - ${this._activeVariant?.language.name} ${this.#renderReadOnlyTag(this._activeVariant?.culture)} - - - ${this._activeVariants.length > 1 - ? html` - - - - ` - : ''} - ` - : nothing - } + ${ref(this.#focusInput)}> + ${this.#hasVariants() + ? html` + + ${this._activeVariant?.language.name} ${this.#renderReadOnlyTag(this._activeVariant?.culture)} + + + ${this._activeVariants.length > 1 + ? html` + + + + ` + : ''} + ` + : html` ${this.#renderReadOnlyTag(null)} `} - ${ - this.#hasVariants() - ? html` - -
- -
    - ${this._variantOptions.map((variant) => this.#renderListItem(variant))} -
-
-
-
- ` - : nothing - } - - ` - : nothing; + ${this.#hasVariants() + ? html` + +
+ +
    + ${this._variantOptions.map((variant) => this.#renderListItem(variant))} +
+
+
+
+ ` + : nothing} + `; } #renderListItem(variantOption: VariantOptionModelType) { @@ -313,17 +304,16 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< } // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected _renderVariantDetails(variantOption: VariantOptionModelType): TemplateResult { + protected _renderVariantDetails(variantOption: VariantOptionModelType) { return html``; } #isReadOnly(culture: string | null) { - if (!culture) return false; return this._readOnlyCultures.includes(culture); } #renderReadOnlyTag(culture?: string | null) { - if (!culture) return nothing; + if (culture === undefined) return nothing; return this.#isReadOnly(culture) ? html`${this.localize.term('general_readOnly')}` : nothing; @@ -376,6 +366,17 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< white-space: nowrap; } + #read-only-tag { + white-space: nowrap; + display: flex; + align-items: center; + justify-content: center; + } + + uui-scroll-container { + max-height: 50dvh; + } + ul { list-style-type: none; padding: 0; @@ -502,6 +503,6 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< declare global { interface HTMLElementTagNameMap { - [elementName]: UmbWorkspaceSplitViewVariantSelectorElement; + 'umb-workspace-split-view-variant-selector': UmbWorkspaceSplitViewVariantSelectorElement; } } 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 574f0e6faf..07e3c570f9 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 @@ -7,6 +7,7 @@ import { UmbEntityContext, type UmbEntityModel, type UmbEntityUnique } from '@um import { UMB_DISCARD_CHANGES_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { + UmbEntityUpdatedEvent, UmbRequestReloadChildrenOfEntityEvent, UmbRequestReloadStructureForEntityEvent, } from '@umbraco-cms/backoffice/entity-action'; @@ -15,6 +16,7 @@ import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/bac import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; import { UmbStateManager } from '@umbraco-cms/backoffice/utils'; import { UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { UmbId } from '@umbraco-cms/backoffice/id'; const LOADING_STATE_UNIQUE = 'umbLoadingEntityDetail'; @@ -45,6 +47,8 @@ export abstract class UmbEntityDetailWorkspaceContextBase< protected _getDataPromise?: Promise; protected _detailRepository?: DetailRepositoryType; + #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) => @@ -85,6 +89,19 @@ export abstract class UmbEntityDetailWorkspaceContextBase< window.addEventListener('willchangestate', this.#onWillNavigate); this.#observeRepository(args.detailRepositoryAlias); this.addValidationContext(this.validationContext); + + this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => { + this.#eventContext = context; + + this.#eventContext.removeEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); + this.#eventContext.addEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); + }); } /** @@ -307,13 +324,21 @@ export abstract class UmbEntityDetailWorkspaceContextBase< this._data.setPersisted(data); this._data.setCurrent(data); - const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); - const event = new UmbRequestReloadStructureForEntityEvent({ - unique: this.getUnique()!, - entityType: this.getEntityType(), + const unique = this.getUnique()!; + const entityType = this.getEntityType(); + + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ unique, entityType }); + + eventContext.dispatchEvent(event); + + const updatedEvent = new UmbEntityUpdatedEvent({ + unique, + entityType, + eventUnique: this._workspaceEventUnique, }); - actionEventContext.dispatchEvent(event); + eventContext.dispatchEvent(updatedEvent); } #allowNavigateAway = false; @@ -396,8 +421,30 @@ export abstract class UmbEntityDetailWorkspaceContextBase< } } + // Discriminator to identify events from this workspace context + protected readonly _workspaceEventUnique = UmbId.new(); + + #onEntityUpdatedEvent = (event: UmbEntityUpdatedEvent) => { + const eventEntityUnique = event.getUnique(); + const eventEntityType = event.getEntityType(); + const eventDiscriminator = event.getEventUnique(); + + // Ignore events for other entities + if (eventEntityType !== this.getEntityType()) return; + if (eventEntityUnique !== this.getUnique()) return; + + // Ignore events from this workspace so we don't reload the data twice. Ex saving this workspace + if (eventDiscriminator === this._workspaceEventUnique) return; + + this.reload(); + }; + public override destroy(): void { window.removeEventListener('willchangestate', this.#onWillNavigate); + this.#eventContext?.removeEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); this._detailRepository?.destroy(); this.#entityContext.destroy(); super.destroy(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts index 04221017dd..055747196e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/index.ts @@ -1,13 +1,11 @@ -import './entity-detail/global-components/index.js'; - export * from './components/index.js'; export * from './conditions/const.js'; export * from './constants.js'; export * from './contexts/index.js'; export * from './controllers/index.js'; -export * from './entity-detail/global-components/index.js'; export * from './entity-detail/index.js'; export * from './entity/index.js'; +export * from './info-app/index.js'; export * from './modals/index.js'; export * from './paths.js'; export * from './submittable/index.js'; @@ -15,4 +13,5 @@ export * from './utils/object-to-property-value-array.function.js'; export * from './workspace-property-dataset/index.js'; export * from './workspace.context-token.js'; export * from './workspace.element.js'; + export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/index.ts new file mode 100644 index 0000000000..7103e66d1e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/index.ts @@ -0,0 +1,3 @@ +import './workspace-info-app-layout.element.js'; + +export * from './workspace-info-app-layout.element.js'; 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 new file mode 100644 index 0000000000..3b6388299e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/global-components/workspace-info-app-layout.element.ts @@ -0,0 +1,31 @@ +import { css, customElement, html, ifDefined, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-workspace-info-app-layout') +export class UmbWorkspaceInfoAppLayoutElement extends UmbLitElement { + @property({ type: String }) + headline?: string; + + protected override render() { + return html` + + + + + `; + } + + static override styles = [ + css` + uui-box { + --uui-box-default-padding: 0; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-info-app-layout': UmbWorkspaceInfoAppLayoutElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/index.ts new file mode 100644 index 0000000000..6b7434bd11 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/index.ts @@ -0,0 +1,3 @@ +import './global-components/index.js'; + +export * from './global-components/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/types.ts new file mode 100644 index 0000000000..8b959f66e4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/types.ts @@ -0,0 +1 @@ +export type * from './workspace-info-app.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/workspace-info-app.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/workspace-info-app.extension.ts new file mode 100644 index 0000000000..19616992aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/info-app/workspace-info-app.extension.ts @@ -0,0 +1,21 @@ +import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; + +export interface UmbWorkspaceInfoAppElement extends HTMLElement { + manifest?: ManifestWorkspaceInfoApp; +} + +export interface ManifestWorkspaceInfoApp + extends ManifestElement, + ManifestWithDynamicConditions { + type: 'workspaceInfoApp'; + meta: MetaWorkspaceInfoApp; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface MetaWorkspaceInfoApp {} + +declare global { + interface UmbExtensionManifestMap { + umbWorkspaceInfoApp: ManifestWorkspaceInfoApp; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts index a641f10887..367ba75712 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts @@ -18,7 +18,6 @@ export abstract class UmbSubmittableWorkspaceContextBase // TODO: We could make a base type for workspace modal data, and use this here: As well as a base for the result, to make sure we always include the unique (instead of the object type) public readonly modalContext?: UmbModalContext<{ preset: object }>; - //public readonly validation = new UmbValidationContext(this); #validationContexts: Array = []; /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts index 76bb7815af..cc9d02132f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/types.ts @@ -1,11 +1,12 @@ import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; -export type * from './extensions/types.js'; -export type * from './kinds/types.js'; export type * from './conditions/types.js'; export type * from './data-manager/types.js'; -export type * from './workspace-context.interface.js'; +export type * from './extensions/types.js'; +export type * from './info-app/types.js'; +export type * from './kinds/types.js'; export type * from './namable/types.js'; +export type * from './workspace-context.interface.js'; /** * @deprecated Use `UmbEntityUnique`instead. 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 8a5e7d6041..608d7e2ffc 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 @@ -26,6 +26,7 @@ export class UmbDataTypeTreeItemTableCollectionViewElement extends UmbLitElement { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts index fdeaf2130b..b987fc3a64 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts @@ -6,7 +6,6 @@ import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; -import { sanitizeHTML } from '@umbraco-cms/backoffice/utils'; @customElement('umb-workspace-view-dictionary-editor') export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { @@ -22,10 +21,6 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { @state() private _currentUserHasAccessToAllLanguages?: boolean = false; - get #dictionaryName() { - return typeof this._dictionary?.name !== 'undefined' ? sanitizeHTML(this._dictionary.name) : '...'; - } - readonly #languageCollectionRepository = new UmbLanguageCollectionRepository(this); #workspaceContext?: typeof UMB_DICTIONARY_WORKSPACE_CONTEXT.TYPE; #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; @@ -89,7 +84,7 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { override render() { return html` - ${this.localize.term('dictionaryItem_description', this.#dictionaryName)} + ${repeat( this._languages, (item) => item.unique, 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 fc1b255111..d2a698f37b 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 @@ -31,6 +31,7 @@ export class UmbDocumentTypeTreeItemTableCollectionViewElement extends UmbLitEle { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-history.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/document-history-workspace-info-app.element.ts similarity index 78% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-history.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/document-history-workspace-info-app.element.ts index cfa0bc151a..264f28b9a4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-history.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/document-history-workspace-info-app.element.ts @@ -1,7 +1,8 @@ -import type { UmbDocumentAuditLogModel } from '../../../audit-log/types.js'; -import { UmbDocumentAuditLogRepository } from '../../../audit-log/index.js'; -import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../document-workspace.context-token.js'; -import { getDocumentHistoryTagStyleAndText, TimeOptions } from './utils.js'; +import { UmbDocumentAuditLogRepository } from '../repository/index.js'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../workspace/constants.js'; +import type { UmbDocumentAuditLogModel } from '../types.js'; +import { TimeOptions } from '../../utils.js'; +import { getDocumentHistoryTagStyleAndText } from './utils.js'; import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPaginationManager } from '@umbraco-cms/backoffice/utils'; @@ -13,8 +14,8 @@ import type { ManifestEntityAction } from '@umbraco-cms/backoffice/entity-action import type { UmbUserItemModel } from '@umbraco-cms/backoffice/user'; import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; -@customElement('umb-document-workspace-view-info-history') -export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement { +@customElement('umb-document-history-workspace-info-app') +export class UmbDocumentHistoryWorkspaceInfoAppElement extends UmbLitElement { #allowedActions = new Set(['Umb.EntityAction.Document.Rollback']); #auditLogRepository = new UmbDocumentAuditLogRepository(this); @@ -98,19 +99,22 @@ export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement { override render() { return html` - + this.#allowedActions.has(manifest.alias)}> - - ${when( - this._items, - () => this.#renderHistory(), - () => html`
`, - )} - ${this.#renderPagination()} -
+ slot="header-actions" + type="entityAction" + .filter=${(manifest: ManifestEntityAction) => + this.#allowedActions.has(manifest.alias)}> + +
+ ${when( + this._items, + () => this.#renderHistory(), + () => html`
`, + )} + ${this.#renderPagination()} +
+ `; } @@ -162,6 +166,11 @@ export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement { static override styles = [ UmbTextStyles, css` + #content { + display: block; + padding: var(--uui-size-space-4) var(--uui-size-space-5); + } + #loader { display: flex; justify-content: center; @@ -173,6 +182,12 @@ export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement { gap: var(--uui-size-layout-1); } + .log-type uui-tag { + height: fit-content; + margin-top: auto; + margin-bottom: auto; + } + uui-pagination { flex: 1; display: flex; @@ -183,10 +198,10 @@ export class UmbDocumentWorkspaceViewInfoHistoryElement extends UmbLitElement { ]; } -export default UmbDocumentWorkspaceViewInfoHistoryElement; +export default UmbDocumentHistoryWorkspaceInfoAppElement; declare global { interface HTMLElementTagNameMap { - 'umb-document-workspace-view-info-history': UmbDocumentWorkspaceViewInfoHistoryElement; + 'umb-document-history-workspace-info-app': UmbDocumentHistoryWorkspaceInfoAppElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/manifests.ts new file mode 100644 index 0000000000..f8860e6add --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/manifests.ts @@ -0,0 +1,18 @@ +import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../../workspace/constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceInfoApp', + name: 'Document History Workspace Info App', + alias: 'Umb.WorkspaceInfoApp.Document.History', + element: () => import('./document-history-workspace-info-app.element.js'), + weight: 80, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/utils.ts similarity index 95% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/utils.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/utils.ts index 5dc112913f..fcf0124809 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/info-app/utils.ts @@ -1,4 +1,4 @@ -import { UmbDocumentAuditLog, type UmbDocumentAuditLogType } from '../../../audit-log/utils/index.js'; +import { UmbDocumentAuditLog, type UmbDocumentAuditLogType } from '../utils/index.js'; interface HistoryStyleMap { look: 'default' | 'primary' | 'secondary' | 'outline' | 'placeholder'; @@ -137,12 +137,3 @@ export function getDocumentHistoryTagStyleAndText(type: UmbDocumentAuditLogType) }; } } - -export const TimeOptions: Intl.DateTimeFormatOptions = { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/manifests.ts new file mode 100644 index 0000000000..9680516a9b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/audit-log/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as infoAppManifests } from './info-app/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [...infoAppManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts index dc2ce0b219..679bb81783 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts @@ -125,7 +125,11 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement { }; }); - this._tableColumns = [...this.#systemColumns, ...userColumns, { name: '', alias: 'entityActions' }]; + this._tableColumns = [ + ...this.#systemColumns, + ...userColumns, + { name: '', alias: 'entityActions', align: 'right' }, + ]; } } 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 6391d67f93..1fad7369cf 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 @@ -1,9 +1,18 @@ import type { UmbDocumentPickerModalData, UmbDocumentPickerModalValue } from '../../modals/types.js'; -import { UMB_DOCUMENT_PICKER_MODAL, UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../constants.js'; +import { + UMB_DOCUMENT_PICKER_MODAL, + UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, + UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS, +} from '../../constants.js'; import type { UmbDocumentItemModel } from '../../repository/index.js'; 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'; + +interface UmbDocumentPickerInputContextOpenArgs { + allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>; +} export class UmbDocumentPickerInputContext extends UmbPickerInputContext< UmbDocumentItemModel, @@ -14,6 +23,46 @@ export class UmbDocumentPickerInputContext extends UmbPickerInputContext< constructor(host: UmbControllerHost) { super(host, UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, UMB_DOCUMENT_PICKER_MODAL, (entry) => entry.unique); } + + override async openPicker( + pickerData?: Partial, + args?: UmbDocumentPickerInputContextOpenArgs, + ) { + const combinedPickerData = { + ...pickerData, + }; + + // transform allowedContentTypes to a pickable filter + combinedPickerData.pickableFilter = (item) => this.#pickableFilter(item, args?.allowedContentTypes); + + // set default search data + if (!pickerData?.search) { + combinedPickerData.search = { + providerAlias: UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS, + ...pickerData?.search, + }; + } + + // pass allowedContentTypes to the search request args + combinedPickerData.search!.queryParams = { + allowedContentTypes: args?.allowedContentTypes, + ...pickerData?.search?.queryParams, + }; + + super.openPicker(combinedPickerData); + } + + #pickableFilter = ( + item: UmbDocumentItemModel, + allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>, + ): boolean => { + if (allowedContentTypes && allowedContentTypes.length > 0) { + return allowedContentTypes + .map((contentTypeReference) => contentTypeReference.unique) + .includes(item.documentType.unique); + } + return true; + }; } /** @deprecated Use `UmbDocumentPickerInputContext` instead. This method will be removed in Umbraco 15. */ 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 05015e1cf8..c16b2c6759 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 @@ -20,6 +20,7 @@ import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import type { UmbDocumentItemModel } from '@umbraco-cms/backoffice/document'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; +import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/document-type'; const elementName = 'umb-input-document'; @@ -175,19 +176,19 @@ export class UmbInputDocumentElement extends UmbFormControlMixin { - if (this.allowedContentTypeIds && this.allowedContentTypeIds.length > 0) { - return this.allowedContentTypeIds.includes(item.documentType.unique); - } - return true; - }; - #openPicker() { - this.#pickerContext.openPicker({ - hideTreeRoot: true, - pickableFilter: this.#pickableFilter, - startNode: this.startNode, - }); + this.#pickerContext.openPicker( + { + hideTreeRoot: true, + startNode: this.startNode, + }, + { + allowedContentTypes: this.allowedContentTypeIds?.map((id) => ({ + unique: id, + entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE, + })), + }, + ); } #onRemove(item: UmbDocumentItemModel) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/constants.ts index b51328d592..456994bdd6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/constants.ts @@ -12,5 +12,6 @@ export * from './reference/constants.js'; export * from './repository/constants.js'; export * from './rollback/constants.js'; export * from './search/constants.js'; +export * from './url/constants.js'; export * from './user-permissions/constants.js'; export * from './workspace/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts index b172085015..fcc6565ced 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts @@ -1,9 +1,9 @@ import './components/index.js'; -export { UmbDocumentAuditLogRepository } from './audit-log/index.js'; +export * from './audit-log/index.js'; export * from './components/index.js'; -export * from './entity-actions/index.js'; export * from './constants.js'; +export * from './entity-actions/index.js'; export * from './global-contexts/index.js'; export * from './modals/index.js'; export * from './paths.js'; @@ -11,6 +11,7 @@ export * from './publishing/index.js'; export * from './recycle-bin/index.js'; export * from './reference/index.js'; export * from './repository/index.js'; +export * from './url/index.js'; export * from './user-permissions/index.js'; export * from './tree/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts index bb29839ea2..e744094a3c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts @@ -1,3 +1,4 @@ +import { manifests as auditLogManifests } from './audit-log/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as entityActionManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests.js'; @@ -13,12 +14,14 @@ import { manifests as rollbackManifests } from './rollback/manifests.js'; import { manifests as searchProviderManifests } from './search/manifests.js'; import { manifests as trackedReferenceManifests } from './reference/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; +import { manifests as urlManifests } from './url/manifests.js'; import { manifests as userPermissionManifests } from './user-permissions/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ + ...auditLogManifests, ...collectionManifests, ...entityActionManifests, ...entityBulkActionManifests, @@ -34,6 +37,7 @@ export const manifests: Array = ...searchProviderManifests, ...trackedReferenceManifests, ...treeManifests, + ...urlManifests, ...userPermissionManifests, ...workspaceManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/workspace-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/workspace-action/manifests.ts index ad68cc5c9a..75854fc50e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/workspace-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/workspace-action/manifests.ts @@ -1,4 +1,5 @@ import { + UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, UMB_USER_PERMISSION_DOCUMENT_PUBLISH, UMB_USER_PERMISSION_DOCUMENT_UPDATE, } from '../../../user-permissions/constants.js'; @@ -20,7 +21,7 @@ export const manifests: Array = [ }, conditions: [ { - alias: 'Umb.Condition.UserPermission.Document', + alias: UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, allOf: [UMB_USER_PERMISSION_DOCUMENT_UPDATE, UMB_USER_PERMISSION_DOCUMENT_PUBLISH], }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/schedule-publish/workspace-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/schedule-publish/workspace-action/manifests.ts index d9697986ca..e61d958fa3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/schedule-publish/workspace-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/schedule-publish/workspace-action/manifests.ts @@ -1,4 +1,5 @@ import { + UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, UMB_USER_PERMISSION_DOCUMENT_PUBLISH, UMB_USER_PERMISSION_DOCUMENT_UPDATE, } from '../../../user-permissions/constants.js'; @@ -19,7 +20,7 @@ export const manifests: Array = [ }, conditions: [ { - alias: 'Umb.Condition.UserPermission.Document', + alias: UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, allOf: [UMB_USER_PERMISSION_DOCUMENT_UPDATE, UMB_USER_PERMISSION_DOCUMENT_PUBLISH], }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/workspace-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/workspace-action/manifests.ts index 30e257a93d..1cd3230e88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/workspace-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/workspace-action/manifests.ts @@ -1,4 +1,7 @@ -import { UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH } from '../../../user-permissions/constants.js'; +import { + UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, + UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, +} from '../../../user-permissions/constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; export const manifests: Array = [ @@ -16,7 +19,7 @@ export const manifests: Array = [ }, conditions: [ { - alias: 'Umb.Condition.UserPermission.Document', + alias: UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, allOf: [UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH], }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-reference.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/info-app/document-references-workspace-view-info.element.ts similarity index 76% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-reference.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/info-app/document-references-workspace-view-info.element.ts index 12cccd21f1..def6b92a80 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-reference.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/info-app/document-references-workspace-view-info.element.ts @@ -1,5 +1,6 @@ -import { UmbDocumentReferenceRepository } from '../../../reference/index.js'; -import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbDocumentReferenceRepository } from '../repository/index.js'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../constants.js'; +import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { isDefaultReference, isDocumentReference, isMediaReference } from '@umbraco-cms/backoffice/relations'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -7,16 +8,10 @@ import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/rou import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import type { UmbReferenceModel } from '@umbraco-cms/backoffice/relations'; import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; -@customElement('umb-document-workspace-view-info-reference') -export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement { - #itemsPerPage = 10; - - #referenceRepository = new UmbDocumentReferenceRepository(this); - - @property() - documentUnique = ''; - +@customElement('umb-document-references-workspace-info-app') +export class UmbDocumentReferencesWorkspaceInfoAppElement extends UmbLitElement { @state() private _editDocumentPath = ''; @@ -29,6 +24,11 @@ export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement @state() private _items?: Array = []; + #itemsPerPage = 10; + #referenceRepository = new UmbDocumentReferenceRepository(this); + #documentUnique?: UmbEntityUnique; + #workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE; + constructor() { super(); @@ -40,15 +40,41 @@ export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement .observeRouteBuilder((routeBuilder) => { this._editDocumentPath = routeBuilder({}); }); + + this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; + this.#observeDocumentUnique(); + }); } - protected override firstUpdated(): void { - this.#getReferences(); + #observeDocumentUnique() { + this.observe( + this.#workspaceContext?.unique, + (unique) => { + if (!unique) { + this.#documentUnique = undefined; + this._items = []; + return; + } + + if (this.#documentUnique === unique) { + return; + } + + this.#documentUnique = unique; + this.#getReferences(); + }, + 'umbReferencesDocumentUniqueObserver', + ); } async #getReferences() { + if (!this.#documentUnique) { + throw new Error('Document unique is required'); + } + const { data } = await this.#referenceRepository.requestReferencedBy( - this.documentUnique, + this.#documentUnique, (this._currentPage - 1) * this.#itemsPerPage, this.#itemsPerPage, ); @@ -111,7 +137,7 @@ export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement override render() { if (!this._items?.length) return nothing; return html` - + @@ -152,8 +178,8 @@ export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement `, )} - - ${this.#renderReferencePagination()} + ${this.#renderReferencePagination()} + `; } @@ -176,6 +202,7 @@ export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement :host { display: contents; } + uui-table-cell:not(.link-cell) { color: var(--uui-color-text-alt); } @@ -194,10 +221,10 @@ export class UmbDocumentWorkspaceViewInfoReferenceElement extends UmbLitElement ]; } -export default UmbDocumentWorkspaceViewInfoReferenceElement; +export default UmbDocumentReferencesWorkspaceInfoAppElement; declare global { interface HTMLElementTagNameMap { - 'umb-document-workspace-view-info-reference': UmbDocumentWorkspaceViewInfoReferenceElement; + 'umb-document-references-workspace-info-app': UmbDocumentReferencesWorkspaceInfoAppElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/info-app/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/info-app/manifests.ts new file mode 100644 index 0000000000..42f6e34b19 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/info-app/manifests.ts @@ -0,0 +1,18 @@ +import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../../workspace/constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceInfoApp', + name: 'Document References Workspace Info App', + alias: 'Umb.WorkspaceInfoApp.Document.References', + element: () => import('./document-references-workspace-view-info.element.js'), + weight: 90, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/manifests.ts index 4ac6fbdcb2..d804039738 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/manifests.ts @@ -1,3 +1,4 @@ +import { manifests as infoAppManifests } from './info-app/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; -export const manifests: Array = [...repositoryManifests]; +export const manifests: Array = [...infoAppManifests, ...repositoryManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/constants.ts index 655e81e66d..7a6c4a9d9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/constants.ts @@ -1,4 +1,3 @@ export * from './detail/constants.js'; export * from './item/constants.js'; -export * from './url/constants.js'; export * from './validation/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/index.ts index 4e403fde93..8059abd30d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/index.ts @@ -1,6 +1,5 @@ export { UmbDocumentDetailRepository } from './detail/index.js'; export { UmbDocumentItemRepository } from './item/index.js'; -export { UmbDocumentUrlRepository, UMB_DOCUMENT_URL_REPOSITORY_ALIAS } from './url/index.js'; export { UmbDocumentPreviewRepository } from './preview/index.js'; export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/manifests.ts index 7c077b7378..4dfb0c911f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/manifests.ts @@ -1,5 +1,4 @@ import { manifests as detailManifests } from './detail/manifests.js'; import { manifests as itemManifests } from './item/manifests.js'; -import { manifests as urlManifests } from './url/manifests.js'; -export const manifests: Array = [...detailManifests, ...itemManifests, ...urlManifests]; +export const manifests: Array = [...detailManifests, ...itemManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/manifests.ts index 83800af9fa..2f736381e6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/manifests.ts @@ -1,9 +1,4 @@ -import { - UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, - UMB_DOCUMENT_ENTITY_TYPE, - UMB_DOCUMENT_WORKSPACE_ALIAS, -} from '../../constants.js'; -import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; +import { UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, UMB_DOCUMENT_ENTITY_TYPE } from '../../constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; export const manifests: Array = [ @@ -12,7 +7,7 @@ export const manifests: Array = [ kind: 'default', alias: 'Umb.EntityAction.Document.Rollback', name: 'Rollback Document Entity Action', - weight: 500, + weight: 450, api: () => import('./rollback.action.js'), forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { @@ -27,12 +22,6 @@ export const manifests: Array = [ { alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, }, - /* Currently the rollback is tightly coupled to the workspace contexts so we only allow it to show up - In the document workspace. */ - { - alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: UMB_DOCUMENT_WORKSPACE_ALIAS, - }, ], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/modal/rollback-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/modal/rollback-modal.element.ts index c8949086cc..e41ad2d053 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/modal/rollback-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/modal/rollback-modal.element.ts @@ -1,5 +1,7 @@ -import { UMB_DOCUMENT_WORKSPACE_CONTEXT, UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../constants.js'; +import { UMB_DOCUMENT_ENTITY_TYPE } from '../../constants.js'; import { UmbRollbackRepository } from '../repository/rollback.repository.js'; +import { UmbDocumentDetailRepository } from '../../repository/index.js'; +import type { UmbDocumentDetailModel } from '../../types.js'; import type { UmbRollbackModalData, UmbRollbackModalValue } from './types.js'; import { diffWords, type Change } from '@umbraco-cms/backoffice/external/diff'; import { css, customElement, html, nothing, repeat, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; @@ -8,8 +10,13 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbUserItemRepository } from '@umbraco-cms/backoffice/user'; import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { UUISelectEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageItemRepository } from '@umbraco-cms/backoffice/language'; +import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import '../../modals/shared/document-variant-language-picker.element.js'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbEntityUpdatedEvent, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action'; type DocumentVersion = { id: string; @@ -22,10 +29,10 @@ type DocumentVersion = { @customElement('umb-rollback-modal') export class UmbRollbackModalElement extends UmbModalBaseElement { @state() - versions: DocumentVersion[] = []; + _versions: DocumentVersion[] = []; @state() - currentVersion?: { + _selectedVersion?: { date: string; name: string; user: string; @@ -37,18 +44,20 @@ export class UmbRollbackModalElement extends UmbModalBaseElement = []; #rollbackRepository = new UmbRollbackRepository(this); #userItemRepository = new UmbUserItemRepository(this); - #workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE; - - #propertyDatasetContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE; - #localizeDateOptions: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', @@ -56,37 +65,76 @@ export class UmbRollbackModalElement extends UmbModalBaseElement { - this.#propertyDatasetContext = instance; - this.currentCulture = instance.getVariantId().culture ?? undefined; - this.#requestVersions(); + this.#currentDatasetCulture = instance.getVariantId().culture ?? undefined; + this.#selectCulture(); }); - this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (instance) => { - this.#workspaceContext = instance; + this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (instance) => { + this.#currentAppCulture = instance.getAppCulture(); + this.#selectCulture(); + }); - this.observe(instance.variantOptions, (options) => { - this.availableVariants = options.map((option) => { + this.consumeContext(UMB_ENTITY_CONTEXT, async (instance) => { + if (instance.getEntityType() !== UMB_DOCUMENT_ENTITY_TYPE) { + throw new Error(`Entity type is not ${UMB_DOCUMENT_ENTITY_TYPE}`); + } + + const unique = instance?.getUnique(); + + if (!unique) { + throw new Error('Document unique is not set'); + } + + const { data } = await new UmbDocumentDetailRepository(this).requestByUnique(unique); + if (!data) return; + + this.#currentDocument = data; + const itemVariants = this.#currentDocument?.variants ?? []; + + this._isInvariant = itemVariants.length === 1 && new UmbVariantId(itemVariants[0].culture).isInvariant(); + this.#selectCulture(); + + const cultures = itemVariants.map((x) => x.culture).filter((x) => x !== null) as string[]; + const { data: languageItems } = await new UmbLanguageItemRepository(this).requestItems(cultures); + + if (languageItems) { + this._availableVariants = languageItems.map((language) => { return { - name: option.language.name, - value: option.language.unique, - selected: option.language.unique === this.currentCulture, + name: language.name, + value: language.unique, + selected: language.unique === this._selectedCulture, }; }); - }); + } else { + this._availableVariants = []; + } + + this.#requestVersions(); }); } + #selectCulture() { + const contextCulture = this.#currentDatasetCulture ?? this.#currentAppCulture ?? null; + this._selectedCulture = this._isInvariant ? null : contextCulture; + } + async #requestVersions() { - if (!this.#propertyDatasetContext) return; + if (!this.#currentDocument?.unique) { + throw new Error('Document unique is not set'); + } - const documentId = this.#propertyDatasetContext.getUnique(); - if (!documentId) return; - - const { data } = await this.#rollbackRepository.requestVersionsByDocumentId(documentId, this.currentCulture); + const { data } = await this.#rollbackRepository.requestVersionsByDocumentId( + this.#currentDocument?.unique, + this._selectedCulture ?? undefined, + ); if (!data) return; const tempItems: DocumentVersion[] = []; @@ -108,28 +156,38 @@ export class UmbRollbackModalElement extends UmbModalBaseElement item.isCurrentlyPublishedVersion)?.id; if (id) { - this.#setCurrentVersion(id); + this.#selectVersion(id); } } - async #setCurrentVersion(id: string) { - const version = this.versions.find((item) => item.id === id); - if (!version) return; + async #selectVersion(id: string) { + const version = this._versions.find((item) => item.id === id); + + if (!version) { + this._selectedVersion = undefined; + this._diffs = []; + return; + } const { data } = await this.#rollbackRepository.requestVersionById(id); - if (!data) return; - this.currentVersion = { + if (!data) { + this._selectedVersion = undefined; + this._diffs = []; + return; + } + + this._selectedVersion = { date: version.date, user: version.user, - name: data.variants.find((x) => x.culture === this.currentCulture)?.name || data.variants[0].name, + name: data.variants.find((x) => x.culture === this._selectedCulture)?.name || data.variants[0].name, id: data.id, properties: data.values - .filter((x) => x.culture === this.currentCulture || !x.culture) // When invariant, culture is undefined or null. + .filter((x) => x.culture === this._selectedCulture || !x.culture) // When invariant, culture is undefined or null. .map((value: any) => { return { alias: value.alias, @@ -137,20 +195,35 @@ export class UmbRollbackModalElement extends UmbModalBaseElement 1 ? this.currentCulture : undefined; - this.#rollbackRepository.rollback(id, culture); + const id = this._selectedVersion.id; + const culture = this._selectedCulture ?? undefined; - const docUnique = this.#workspaceContext?.getUnique() ?? ''; - // TODO Use the load method on the context instead of location.href, when it works. - // this.#workspaceContext?.load(docUnique); - location.href = UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateAbsolute({ unique: docUnique }); - this.modalContext?.reject(); + const { error } = await this.#rollbackRepository.rollback(id, culture); + if (error) return; + + const unique = this.#currentDocument?.unique; + const entityType = this.#currentDocument?.entityType; + + if (!unique || !entityType) { + throw new Error('Document unique or entity type is not set'); + } + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + + const reloadStructureEvent = new UmbRequestReloadStructureForEntityEvent({ unique, entityType }); + actionEventContext.dispatchEvent(reloadStructureEvent); + + const entityUpdatedEvent = new UmbEntityUpdatedEvent({ unique, entityType }); + actionEventContext.dispatchEvent(entityUpdatedEvent); + + this.modalContext?.submit(); } #onCancel() { @@ -158,7 +231,7 @@ export class UmbRollbackModalElement extends UmbModalBaseElement item.id === id); + const version = this._versions.find((item) => item.id === id); if (!version) return; version.preventCleanup = preventCleanup; @@ -176,125 +249,147 @@ export class UmbRollbackModalElement extends UmbModalBaseElement - ${this.localize.term('general_language')} - - + `; } #renderVersions() { - return html` ${this.#renderCultureSelect()} - ${repeat( - this.versions, - (item) => item.id, - (item) => { - return html` -
this.#onVersionClicked(item.id)} - @keydown=${() => {}} - class="rollback-item ${this.currentVersion?.id === item.id ? 'active' : ''}"> -
-

- -

-

${item.user}

-

${item.isCurrentlyPublishedVersion ? this.localize.term('rollback_currentPublishedVersion') : ''}

+ if (!this._versions.length) { + return html`No versions available`; + } + + return html` + ${repeat( + this._versions, + (item) => item.id, + (item) => { + return html` +
this.#onVersionClicked(item.id)} + @keydown=${() => {}} + class="rollback-item ${this._selectedVersion?.id === item.id ? 'active' : ''}"> +
+

+ +

+

${item.user}

+

${item.isCurrentlyPublishedVersion ? this.localize.term('rollback_currentPublishedVersion') : ''}

+
+ this.#onPreventCleanup(event, item.id, !item.preventCleanup)} + label=${item.preventCleanup + ? this.localize.term('contentTypeEditor_historyCleanupEnableCleanup') + : this.localize.term('contentTypeEditor_historyCleanupPreventCleanup')}>
- this.#onPreventCleanup(event, item.id, !item.preventCleanup)} - label=${item.preventCleanup - ? this.localize.term('contentTypeEditor_historyCleanupEnableCleanup') - : this.localize.term('contentTypeEditor_historyCleanupPreventCleanup')}> -
- `; - }, - )}`; + `; + }, + )}`; } - #renderCurrentVersion() { - if (!this.currentVersion) return; + async #setDiffs() { + if (!this._selectedVersion) return; - let draftValues = - (this.#workspaceContext?.getData()?.values as Array<{ alias: string; culture: string; value: any }>) ?? []; + const currentPropertyValues = this.#currentDocument?.values.filter( + (x) => x.culture === this._selectedCulture || !x.culture, + ); // When invariant, culture is undefined or null. - draftValues = draftValues.filter((x) => x.culture === this.currentCulture || !x.culture); // When invariant, culture is undefined or null. + if (!currentPropertyValues) { + throw new Error('Current property values are not set'); + } + + const currentName = this.#currentDocument?.variants.find((x) => x.culture === this._selectedCulture)?.name; + + if (!currentName) { + throw new Error('Current name is not set'); + } const diffs: Array<{ alias: string; diff: Change[] }> = []; - const nameDiff = diffWords(this.#workspaceContext?.getName() ?? '', this.currentVersion.name); + const nameDiff = diffWords(currentName, this._selectedVersion.name); diffs.push({ alias: 'name', diff: nameDiff }); - this.currentVersion.properties.forEach((item) => { - const draftValue = draftValues.find((x) => x.alias === item.alias); + this._selectedVersion.properties.forEach((item) => { + const draftValue = currentPropertyValues.find((x) => x.alias === item.alias); if (!draftValue) return; - const draftValueString = trimQuotes(JSON.stringify(draftValue.value)); - const versionValueString = trimQuotes(JSON.stringify(item.value)); + const draftValueString = this.#trimQuotes(JSON.stringify(draftValue.value)); + const versionValueString = this.#trimQuotes(JSON.stringify(item.value)); const diff = diffWords(draftValueString, versionValueString); diffs.push({ alias: item.alias, diff }); }); - /** - * - * @param str - */ - function trimQuotes(str: string): string { - return str.replace(/^['"]|['"]$/g, ''); - } + this._diffs = [...diffs]; + } + + #renderSelectedVersion() { + if (!this._selectedVersion) + return html` + No selected version + `; return html` - ${unsafeHTML(this.localize.term('rollback_diffHelp'))} - - - + + ${unsafeHTML(this.localize.term('rollback_diffHelp'))} + + + - - ${this.localize.term('general_alias')} - ${this.localize.term('general_value')} - - ${repeat( - diffs, - (item) => item.alias, - (item) => { - const diff = diffs.find((x) => x?.alias === item.alias); - return html` - - ${item.alias} - - ${diff - ? diff.diff.map((part) => - part.added - ? html`${part.value}` - : part.removed - ? html`${part.value}` - : part.value, - ) - : nothing} - - - `; - }, - )} - + + ${this.localize.term('general_alias')} + ${this.localize.term('general_value')} + + ${repeat( + this._diffs, + (item) => item.alias, + (item) => { + const diff = this._diffs.find((x) => x?.alias === item.alias); + return html` + + ${item.alias} + + ${diff + ? diff.diff.map((part) => + part.added + ? html`${part.value}` + : part.removed + ? html`${part.value}` + : part.value, + ) + : nothing} + + + `; + }, + )} + + `; } get currentVersionHeader() { return ( - this.localize.date(this.currentVersion?.date ?? new Date(), this.#localizeDateOptions) + + this.localize.date(this._selectedVersion?.date ?? new Date(), this.#localizeDateOptions) + ' - ' + - this.currentVersion?.user + this._selectedVersion?.user ); } @@ -302,10 +397,17 @@ export class UmbRollbackModalElement extends UmbModalBaseElement
- -
${this.#renderVersions()}
-
- ${this.#renderCurrentVersion()} +
+ ${this._availableVariants.length + ? html` + + ${this.#renderCultureSelect()} + + ` + : nothing} + ${this.#renderVersions()} +
+ ${this.#renderSelectedVersion()}
+ label=${this.localize.term('actions_rollback')} + ?disabled=${!this._selectedVersion}> `; @@ -329,14 +432,15 @@ export class UmbRollbackModalElement extends UmbModalBaseElement>>} - The search results + * @memberof UmbDocumentSearchRepository + */ + search( + args: UmbDocumentSearchRequestArgs, + ): Promise>> { return this.#dataSource.search(args); } } 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 961fdfd54d..bfb561c99f 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 @@ -1,6 +1,6 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; -import type { UmbDocumentSearchItemModel } from './types.js'; -import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbDocumentSearchItemModel, UmbDocumentSearchRequestArgs } from './types.js'; +import type { UmbSearchDataSource } from '@umbraco-cms/backoffice/search'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -10,7 +10,9 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbDocumentSearchServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbDocumentSearchServerDataSource implements UmbSearchDataSource { +export class UmbDocumentSearchServerDataSource + implements UmbSearchDataSource +{ #host: UmbControllerHost; /** @@ -24,16 +26,17 @@ export class UmbDocumentSearchServerDataSource implements UmbSearchDataSource contentType.unique), }), ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document.search-provider.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document.search-provider.ts index 6e9a94559f..1dfb88a990 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document.search-provider.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/document.search-provider.ts @@ -1,15 +1,30 @@ import { UmbDocumentSearchRepository } from './document-search.repository.js'; -import type { UmbDocumentSearchItemModel } from './types.js'; -import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbDocumentSearchItemModel, UmbDocumentSearchRequestArgs } from './types.js'; +import type { UmbSearchProvider } from '@umbraco-cms/backoffice/search'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbPagedModel, UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository'; +/** + * The document search provider + * @class UmbDocumentSearchProvider + * @augments {UmbControllerBase} + * @implements {UmbSearchProvider} + */ export class UmbDocumentSearchProvider extends UmbControllerBase - implements UmbSearchProvider + implements UmbSearchProvider { #repository = new UmbDocumentSearchRepository(this); - search(args: UmbSearchRequestArgs) { + /** + * Search for documents + * @param {UmbDocumentSearchRequestArgs} args - The arguments for the search + * @returns {Promise>>} - The search results + * @memberof UmbDocumentSearchProvider + */ + search( + args: UmbDocumentSearchRequestArgs, + ): Promise>> { return this.#repository.search(args); } 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 ff91e7037c..a63f89853b 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 @@ -1,5 +1,11 @@ import type { UmbDocumentItemModel } from '../repository/index.js'; +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbDocumentTypeEntityType } from '@umbraco-cms/backoffice/document-type'; export interface UmbDocumentSearchItemModel extends UmbDocumentItemModel { href: string; } + +export interface UmbDocumentSearchRequestArgs extends UmbSearchRequestArgs { + allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/constants.ts new file mode 100644 index 0000000000..41a409dec1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/constants.ts @@ -0,0 +1 @@ +export * from './repository/constants.js'; 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 new file mode 100644 index 0000000000..03eef61de7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/index.ts @@ -0,0 +1,3 @@ +export { UmbDocumentUrlRepository, UMB_DOCUMENT_URL_REPOSITORY_ALIAS } from './repository/index.js'; + +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-links.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts similarity index 90% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-links.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts index 4024d22977..bfd6d56d45 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-links.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/document-links-workspace-info-app.element.ts @@ -1,7 +1,7 @@ -import { UmbDocumentUrlRepository } from '../../../repository/url/document-url.repository.js'; -import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../document-workspace.context-token.js'; -import type { UmbDocumentVariantOptionModel } from '../../../types.js'; -import type { UmbDocumentUrlModel } from '../../../repository/url/types.js'; +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 { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action'; @@ -18,8 +18,8 @@ interface UmbDocumentInfoViewLink { state: DocumentVariantStateModel | null | undefined; } -@customElement('umb-document-workspace-view-info-links') -export class UmbDocumentWorkspaceViewInfoLinksElement extends UmbLitElement { +@customElement('umb-document-links-workspace-info-app') +export class UmbDocumentLinksWorkspaceInfoAppElement extends UmbLitElement { #documentUrlRepository = new UmbDocumentUrlRepository(this); @state() @@ -155,13 +155,13 @@ export class UmbDocumentWorkspaceViewInfoLinksElement extends UmbLitElement { override render() { return html` - + ${when( this._loading, () => this.#renderLoading(), () => this.#renderContent(), )} - + `; } @@ -257,8 +257,7 @@ export class UmbDocumentWorkspaceViewInfoLinksElement extends UmbLitElement { justify-content: space-between; align-items: center; gap: var(--uui-size-6); - - padding: var(--uui-size-space-4) var(--uui-size-space-6); + padding: var(--uui-size-space-4) var(--uui-size-space-5); &:is(a) { cursor: pointer; @@ -284,10 +283,10 @@ export class UmbDocumentWorkspaceViewInfoLinksElement extends UmbLitElement { ]; } -export default UmbDocumentWorkspaceViewInfoLinksElement; +export default UmbDocumentLinksWorkspaceInfoAppElement; declare global { interface HTMLElementTagNameMap { - 'umb-document-workspace-view-info-links': UmbDocumentWorkspaceViewInfoLinksElement; + 'umb-document-links-workspace-info-app': UmbDocumentLinksWorkspaceInfoAppElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/manifests.ts new file mode 100644 index 0000000000..58a7862ff3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/info-app/manifests.ts @@ -0,0 +1,18 @@ +import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../../workspace/constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceInfoApp', + name: 'Document Links Workspace Info App', + alias: 'Umb.WorkspaceInfoApp.Document.Links', + element: () => import('./document-links-workspace-info-app.element.js'), + weight: 100, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/manifests.ts new file mode 100644 index 0000000000..f241b57605 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as infoAppManifests } from './info-app/manifests.js'; +import { manifests as repositoryManifests } from './repository/manifests.js'; + +export const manifests: Array = [...repositoryManifests, ...infoAppManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/document-url.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.repository.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/document-url.repository.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.repository.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/document-url.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.server.data-source.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/document-url.server.data-source.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.server.data-source.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/document-url.store.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.store.context-token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/document-url.store.context-token.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.store.context-token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/document-url.store.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.store.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/document-url.store.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/document-url.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/types.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/url/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/url/repository/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/document-user-permission.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/document-user-permission.condition.ts index 64d5836673..060e758808 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/document-user-permission.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/document-user-permission.condition.ts @@ -3,25 +3,31 @@ import type { UmbDocumentUserPermissionConditionConfig } from './types.js'; import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; -import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { DocumentPermissionPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +// Do not export - for internal use only +type UmbOnChangeCallbackType = (permitted: boolean) => void; + +export class UmbDocumentUserPermissionCondition extends UmbControllerBase implements UmbExtensionCondition { + config: UmbDocumentUserPermissionConditionConfig; + permitted = false; -export class UmbDocumentUserPermissionCondition - extends UmbConditionBase - implements UmbExtensionCondition -{ #entityType: string | undefined; #unique: string | null | undefined; #documentPermissions: Array = []; #fallbackPermissions: string[] = []; + #onChange: UmbOnChangeCallbackType; constructor( host: UmbControllerHost, - args: UmbConditionControllerArguments, + args: UmbConditionControllerArguments, ) { - super(host, args); + super(host); + this.config = args.config; + this.#onChange = args.onChange; this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => { this.observe( @@ -102,7 +108,11 @@ export class UmbDocumentUserPermissionCondition oneOfPermitted = false; } - this.permitted = allOfPermitted && oneOfPermitted; + const permitted = allOfPermitted && oneOfPermitted; + if (permitted === this.permitted) return; + + this.permitted = permitted; + this.#onChange(permitted); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts index a7e8ea7e4d..3622652b51 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts @@ -37,3 +37,12 @@ export const sortVariants = (a: VariantType, b: VariantType) => { return compareDefault(a, b) || compareMandatory(a, b) || compareState(a, b) || compareName(a, b); }; + +export const TimeOptions: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/manifests.ts new file mode 100644 index 0000000000..291ff61745 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/manifests.ts @@ -0,0 +1,48 @@ +import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../constants.js'; +import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceAction', + kind: 'default', + alias: 'Umb.WorkspaceAction.Document.Save', + name: 'Save Document Workspace Action', + weight: 80, + api: () => import('./save.action.js'), + meta: { + label: '#buttons_save', + look: 'secondary', + color: 'positive', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_WORKSPACE_ALIAS, + }, + { + alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, + }, + ], + }, + { + type: 'workspaceAction', + kind: 'default', + alias: 'Umb.WorkspaceAction.Document.SaveAndPreview', + name: 'Save And Preview Document Workspace Action', + weight: 90, + api: () => import('./save-and-preview.action.js'), + meta: { + label: '#buttons_saveAndPreview', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_WORKSPACE_ALIAS, + }, + { + alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts index f848baf0b7..8ef0251705 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts @@ -1,38 +1,56 @@ -import { - UMB_USER_PERMISSION_DOCUMENT_CREATE, - UMB_USER_PERMISSION_DOCUMENT_UPDATE, -} from '../../user-permissions/constants.js'; -import { UmbDocumentUserPermissionCondition } from '../../user-permissions/conditions/document-user-permission.condition.js'; +import type { UmbDocumentVariantModel } from '../../types.js'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../document-workspace.context-token.js'; +import type UmbDocumentWorkspaceContext from '../document-workspace.context.js'; +import type { UmbVariantState } from '@umbraco-cms/backoffice/utils'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbSubmitWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; export class UmbDocumentSaveWorkspaceAction extends UmbSubmitWorkspaceAction { + #documentWorkspaceContext?: UmbDocumentWorkspaceContext; + #variants: Array = []; + #readOnlyStates: Array = []; + constructor(host: UmbControllerHost, args: any) { super(host, args); - /* The action is disabled by default because the onChange callback - will first be triggered when the condition is changed to permitted */ - this.disable(); - - // TODO: this check is not sufficient. It will show the save button if a use - // has only create options. The best solution would be to split the two buttons into two separate actions - // with a condition on isNew to show/hide them - // The server will throw a permission error if this scenario happens - const condition = new UmbDocumentUserPermissionCondition(host, { - host, - config: { - alias: 'Umb.Condition.UserPermission.Document', - oneOf: [UMB_USER_PERMISSION_DOCUMENT_CREATE, UMB_USER_PERMISSION_DOCUMENT_UPDATE], - }, - onChange: () => { - if (condition.permitted) { - this.enable(); - } else { - this.disable(); - } - }, + this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => { + this.#documentWorkspaceContext = context; + this.#observeVariants(); + this.#observeReadOnlyStates(); }); } + + #observeVariants() { + this.observe( + this.#documentWorkspaceContext?.variants, + (variants) => { + this.#variants = variants ?? []; + this.#check(); + }, + 'saveWorkspaceActionVariantsObserver', + ); + } + + #observeReadOnlyStates() { + this.observe( + this.#documentWorkspaceContext?.readOnlyState.states, + (readOnlyStates) => { + this.#readOnlyStates = readOnlyStates ?? []; + this.#check(); + }, + 'saveWorkspaceActionReadOnlyStatesObserver', + ); + } + + #check() { + const allVariantsAreReadOnly = this.#variants.every((variant) => { + const variantId = new UmbVariantId(variant.culture, variant.segment); + return this.#readOnlyStates.some((state) => state.variantId.equal(variantId)); + }); + + return allVariantsAreReadOnly ? this.disable() : this.enable(); + } } export { UmbDocumentSaveWorkspaceAction as api }; 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 c4eb63ebf7..d709465d31 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 @@ -9,7 +9,10 @@ import { UMB_DOCUMENT_COLLECTION_ALIAS, UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_SAVE_MODAL, + UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN, + UMB_USER_PERMISSION_DOCUMENT_CREATE, + UMB_USER_PERMISSION_DOCUMENT_UPDATE, } from '../constants.js'; import { UmbDocumentPreviewRepository } from '../repository/preview/index.js'; import { UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT, UmbDocumentPublishingRepository } from '../publishing/index.js'; @@ -33,6 +36,7 @@ import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/documen import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; type ContentModel = UmbDocumentDetailModel; type ContentTypeModel = UmbDocumentTypeDetailModel; @@ -66,6 +70,8 @@ export class UmbDocumentWorkspaceContext #isTrashedContext = new UmbIsTrashedEntityContext(this); #publishingContext?: typeof UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT.TYPE; + #userCanCreate = false; + #userCanUpdate = false; constructor(host: UmbControllerHost) { super(host, { @@ -86,6 +92,28 @@ export class UmbDocumentWorkspaceContext this.#publishingContext = context; }); + createExtensionApiByAlias(this, UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, [ + { + config: { + allOf: [UMB_USER_PERMISSION_DOCUMENT_CREATE], + }, + onChange: (permitted: boolean) => { + this.#userCanCreate = permitted; + }, + }, + ]); + + createExtensionApiByAlias(this, UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, [ + { + config: { + allOf: [UMB_USER_PERMISSION_DOCUMENT_UPDATE], + }, + onChange: (permitted: boolean) => { + this.#userCanUpdate = permitted; + }, + }, + ]); + this.routes.setRoutes([ { path: UMB_CREATE_FROM_BLUEPRINT_DOCUMENT_WORKSPACE_PATH_PATTERN.toString(), @@ -102,6 +130,7 @@ export class UmbDocumentWorkspaceContext documentTypeUnique, blueprintUnique, ); + new UmbWorkspaceIsNewRedirectController( this, this, @@ -118,6 +147,12 @@ export class UmbDocumentWorkspaceContext const documentTypeUnique = info.match.params.documentTypeUnique; await this.create({ entityType: parentEntityType, unique: parentUnique }, documentTypeUnique); + this.#setReadOnlyStateForUserPermission( + UMB_USER_PERMISSION_DOCUMENT_CREATE, + this.#userCanCreate, + 'You do not have permission to create documents.', + ); + new UmbWorkspaceIsNewRedirectController( this, this, @@ -128,10 +163,15 @@ export class UmbDocumentWorkspaceContext { path: UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.toString(), component: () => import('./document-workspace-editor.element.js'), - setup: (_component, info) => { + setup: async (_component, info) => { this.removeUmbControllerByAlias(UmbWorkspaceIsNewRedirectControllerAlias); const unique = info.match.params.unique; - this.load(unique); + await this.load(unique); + this.#setReadOnlyStateForUserPermission( + UMB_USER_PERMISSION_DOCUMENT_UPDATE, + this.#userCanUpdate, + 'You do not have permission to update documents.', + ); }, }, ]); @@ -326,6 +366,30 @@ export class UmbDocumentWorkspaceContext ): UmbDocumentPropertyDatasetContext { return new UmbDocumentPropertyDatasetContext(host, this, variantId); } + + async #setReadOnlyStateForUserPermission(identifier: string, permitted: boolean, message: string) { + const variants = this.getVariants(); + const uniques = variants?.map((variant) => identifier + variant.culture) || []; + + if (permitted) { + this.readOnlyState?.removeStates(uniques); + return; + } + + const variantIds = variants?.map((variant) => new UmbVariantId(variant.culture, variant.segment)) || []; + + const readOnlyStates = variantIds.map((variantId) => { + return { + unique: identifier + variantId.culture, + variantId, + message, + }; + }); + + this.readOnlyState?.removeStates(uniques); + + this.readOnlyState?.addStates(readOnlyStates); + } } export default UmbDocumentWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/index.ts new file mode 100644 index 0000000000..5414064f66 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/index.ts @@ -0,0 +1 @@ +export * from './document-workspace.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts index 4132437d96..6c18d54faa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts @@ -1,6 +1,6 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './constants.js'; -import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; +import { manifests as actionManifests } from './actions/manifests.js'; import { UMB_CONTENT_HAS_PROPERTIES_WORKSPACE_CONDITION } from '@umbraco-cms/backoffice/content'; import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; @@ -74,47 +74,5 @@ export const manifests: Array = [ }, ], }, - - { - type: 'workspaceAction', - kind: 'default', - alias: 'Umb.WorkspaceAction.Document.Save', - name: 'Save Document Workspace Action', - weight: 80, - api: () => import('./actions/save.action.js'), - meta: { - label: '#buttons_save', - look: 'secondary', - color: 'positive', - }, - conditions: [ - { - alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: UMB_DOCUMENT_WORKSPACE_ALIAS, - }, - { - alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, - }, - ], - }, - { - type: 'workspaceAction', - kind: 'default', - alias: 'Umb.WorkspaceAction.Document.SaveAndPreview', - name: 'Save And Preview Document Workspace Action', - weight: 90, - api: () => import('./actions/save-and-preview.action.js'), - meta: { - label: '#buttons_saveAndPreview', - }, - conditions: [ - { - alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: UMB_DOCUMENT_WORKSPACE_ALIAS, - }, - { - alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, - }, - ], - }, + ...actionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts index 0e7c96dadf..d21a6a2fcd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts @@ -1,7 +1,7 @@ import { UMB_DOCUMENT_PROPERTY_DATASET_CONTEXT, UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../../constants.js'; import type { UmbDocumentVariantModel } from '../../../types.js'; import { UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT } from '../../../publishing/index.js'; -import { TimeOptions } from './utils.js'; +import { TimeOptions } from '../../../utils.js'; import { css, customElement, html, ifDefined, nothing, state } from '@umbraco-cms/backoffice/external/lit'; import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -15,11 +15,6 @@ import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-reg import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; -// import of local components -import './document-workspace-view-info-links.element.js'; -import './document-workspace-view-info-history.element.js'; -import './document-workspace-view-info-reference.element.js'; - @customElement('umb-document-workspace-view-info') export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { @state() @@ -182,10 +177,7 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { override render() { return html`
- - - +
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 64f6e4b90b..34017d758a 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 @@ -34,6 +34,7 @@ export class UmbExtensionTableCollectionViewElement extends UmbLitElement { { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/health-check/health-check-dashboard.context.ts b/src/Umbraco.Web.UI.Client/src/packages/health-check/health-check-dashboard.context.ts index 36a1b3f7b4..2801a13365 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/health-check/health-check-dashboard.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/health-check/health-check-dashboard.context.ts @@ -20,9 +20,9 @@ export class UmbHealthCheckDashboardContext { this.host = host; } - checkAll() { + async checkAll() { for (const [label, api] of this.apis.entries()) { - api?.checkGroup?.(label); + await api?.checkGroup?.(label); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group.element.ts index d07ccf63f9..00bae1f180 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-group.element.ts @@ -54,7 +54,7 @@ export class UmbDashboardHealthCheckGroupElement extends UmbLitElement { private async _buttonHandler() { this._buttonState = 'waiting'; - this._api?.checkGroup(this.groupName); + await this._api?.checkGroup(this.groupName); this._buttonState = 'success'; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-overview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-overview.element.ts index 7377edfd7b..25f69d3606 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-overview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/health-check/views/health-check-overview.element.ts @@ -23,7 +23,9 @@ export class UmbDashboardHealthCheckOverviewElement extends UmbLitElement { } private async _onHealthCheckHandler() { - this._healthCheckDashboardContext?.checkAll(); + this._buttonState = 'waiting'; + await this._healthCheckDashboardContext?.checkAll(); + this._buttonState = 'success'; } override render() { @@ -41,7 +43,13 @@ export class UmbDashboardHealthCheckOverviewElement extends UmbLitElement {
- + + ${ + // As well as the visual presentation, this amend to the rendering based on button state is necessary + // in order to trigger an update after the checks are complete (this.requestUpdate() doesn't suffice). + this._buttonState !== 'waiting' + ? html`` + : html``}
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 1a37448b3f..c7a379909a 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 @@ -41,6 +41,7 @@ export class UmbLanguageTableCollectionViewElement extends UmbLitElement { { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts index 39e3004534..b85eb539fc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts @@ -225,7 +225,7 @@ export class UmbInputMarkdownElement extends UmbFormControlMixin(UmbLitElement, .then(async (value) => { if (!value) return; - const uniques = value.selection; + const uniques = value.selection.filter((unique) => unique !== null) as Array; const { data: mediaUrls } = await this.#mediaUrlRepository.requestItems(uniques); const mediaUrl = mediaUrls?.length ? (mediaUrls[0].url ?? 'URL') : 'URL'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/media-image.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/media-image.element.ts index c81596b112..517bfab56b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/media-image.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/media-image.element.ts @@ -1,4 +1,4 @@ -import { UmbMediaUrlRepository } from '../../media/repository/index.js'; +import { UmbMediaUrlRepository } from '../../media/url/index.js'; import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; 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 439728ef92..8ae49c7691 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 @@ -26,6 +26,7 @@ export class UmbMediaTypeTreeItemTableCollectionViewElement extends UmbLitElemen { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/manifests.ts new file mode 100644 index 0000000000..d79e6b6fba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/manifests.ts @@ -0,0 +1,18 @@ +import { UMB_MEDIA_WORKSPACE_ALIAS } from '../../workspace/constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceInfoApp', + name: 'Media History Workspace Info App', + alias: 'Umb.WorkspaceInfoApp.Media.History', + element: () => import('./media-history-workspace-info-app.element.js'), + weight: 80, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_MEDIA_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info-history.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/media-history-workspace-info-app.element.ts similarity index 75% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info-history.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/media-history-workspace-info-app.element.ts index 33ac9a609f..58fa25757a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info-history.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/media-history-workspace-info-app.element.ts @@ -1,8 +1,8 @@ -import type { UmbMediaAuditLogModel } from '../../../audit-log/types.js'; -import { UmbMediaAuditLogRepository } from '../../../audit-log/index.js'; -import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../media-workspace.context-token.js'; -import { TimeOptions, getMediaHistoryTagStyleAndText } from './utils.js'; -import { css, html, customElement, state, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../workspace/constants.js'; +import type { UmbMediaAuditLogModel } from '../types.js'; +import { UmbMediaAuditLogRepository } from '../repository/index.js'; +import { getMediaHistoryTagStyleAndText, TimeOptions } from './utils.js'; +import { css, html, customElement, state, nothing, repeat, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbPaginationManager } from '@umbraco-cms/backoffice/utils'; @@ -10,8 +10,8 @@ import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; import type { UmbUserItemModel } from '@umbraco-cms/backoffice/user'; import { UmbUserItemRepository } from '@umbraco-cms/backoffice/user'; -@customElement('umb-media-workspace-view-info-history') -export class UmbMediaWorkspaceViewInfoHistoryElement extends UmbLitElement { +@customElement('umb-media-history-workspace-info-app') +export class UmbMediaHistoryWorkspaceInfoAppElement extends UmbLitElement { @state() _currentPageNumber = 1; @@ -83,13 +83,18 @@ export class UmbMediaWorkspaceViewInfoHistoryElement extends UmbLitElement { } override render() { - return html` -
-

History

-
- ${this._items ? this.#renderHistory() : html` `} - ${this.#renderPagination()} -
`; + return html` + +
+ ${when( + this._items, + () => this.#renderHistory(), + () => html`
`, + )} + ${this.#renderPagination()} +
+
+ `; } #renderHistory() { @@ -145,25 +150,30 @@ export class UmbMediaWorkspaceViewInfoHistoryElement extends UmbLitElement { static override styles = [ UmbTextStyles, css` - uui-loader-circle { - font-size: 2rem; + #content { + display: block; + padding: var(--uui-size-space-4) var(--uui-size-space-5); } - uui-tag uui-icon { - margin-right: var(--uui-size-space-1); + #loader { + display: flex; + justify-content: center; } .log-type { - flex-grow: 1; - gap: var(--uui-size-space-2); + display: grid; + grid-template-columns: var(--uui-size-40) auto; + gap: var(--uui-size-layout-1); + } + + .log-type uui-tag { + height: fit-content; + margin-top: auto; + margin-bottom: auto; } uui-pagination { flex: 1; - display: inline-block; - } - - .pagination { display: flex; justify-content: center; margin-top: var(--uui-size-layout-1); @@ -172,10 +182,10 @@ export class UmbMediaWorkspaceViewInfoHistoryElement extends UmbLitElement { ]; } -export default UmbMediaWorkspaceViewInfoHistoryElement; +export default UmbMediaHistoryWorkspaceInfoAppElement; declare global { interface HTMLElementTagNameMap { - 'umb-media-workspace-view-info-history': UmbMediaWorkspaceViewInfoHistoryElement; + 'umb-media-workspace-view-info-history': UmbMediaHistoryWorkspaceInfoAppElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/utils.ts similarity index 87% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/utils.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/utils.ts index f4290ab2c0..e410f350d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/utils.ts @@ -1,5 +1,5 @@ -import type { UmbMediaAuditLogType } from '../../../audit-log/utils/index.js'; -import { UmbMediaAuditLog } from '../../../audit-log/utils/index.js'; +import type { UmbMediaAuditLogType } from '../utils/index.js'; +import { UmbMediaAuditLog } from '../utils/index.js'; interface HistoryStyleMap { look: 'default' | 'primary' | 'secondary' | 'outline' | 'placeholder'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/manifests.ts new file mode 100644 index 0000000000..c72138184b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as infoAppManifests } from './info-app/manifests.js'; + +export const manifests: Array = [...infoAppManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts index ff7cf3939d..8353b3579e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts @@ -48,6 +48,11 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< this.#placeholders.updateOne(unique, { status }); } + updatePlaceholderProgress(unique: string, progress: number) { + this._items.updateOne(unique, { progress }); + this.#placeholders.updateOne(unique, { progress }); + } + /** * Requests the collection from the repository. * @returns {Promise} 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 c2625da049..e5deb42fa9 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 @@ -1,9 +1,9 @@ import { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE } from '../entity.js'; import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../workspace/media-workspace.context-token.js'; -import { UmbFileDropzoneItemStatus, type UmbUploadableItem } from '../dropzone/types.js'; +import type { UmbDropzoneSubmittedEvent } from '../dropzone/dropzone-submitted.event.js'; import type { UmbDropzoneElement } from '../dropzone/dropzone.element.js'; import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js'; -import { customElement, html, query, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, ref, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; @@ -18,9 +18,6 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { @state() private _unique: string | null = null; - @query('#dropzone') - private _dropzone!: UmbDropzoneElement; - constructor() { super(); @@ -35,45 +32,47 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { }); } - #observeProgressItems() { + #observeProgressItems(dropzone?: Element) { + if (!dropzone) return; this.observe( - this._dropzone.progressItems(), + (dropzone as UmbDropzoneElement).progressItems(), (progressItems) => { progressItems.forEach((item) => { - if (item.status === UmbFileDropzoneItemStatus.COMPLETE && !item.folder?.name) { - // We do not update folders as it may have children still being uploaded. - this.#collectionContext?.updatePlaceholderStatus(item.unique, UmbFileDropzoneItemStatus.COMPLETE); - } + // We do not update folders as it may have children still being uploaded. + if (item.folder?.name) return; + + this.#collectionContext?.updatePlaceholderStatus(item.unique, item.status); + this.#collectionContext?.updatePlaceholderProgress(item.unique, item.progress); }); }, '_observeProgressItems', ); } - async #setupPlaceholders(event: CustomEvent) { + async #setupPlaceholders(event: UmbDropzoneSubmittedEvent) { event.preventDefault(); - const uploadable = event.detail as Array; - const placeholders = uploadable + const placeholders = event.items .filter((p) => p.parentUnique === this._unique) .map((p) => ({ unique: p.unique, status: p.status, name: p.temporaryFile?.file.name ?? p.folder?.name })); this.#collectionContext?.setPlaceholders(placeholders); - this.#observeProgressItems(); } - async #onComplete() { + async #onComplete(event: Event) { + event.preventDefault(); this._progress = -1; this.#collectionContext?.requestCollection(); const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); - const event = new UmbRequestReloadChildrenOfEntityEvent({ + const reloadEvent = new UmbRequestReloadChildrenOfEntityEvent({ entityType: this._unique ? UMB_MEDIA_ENTITY_TYPE : UMB_MEDIA_ROOT_ENTITY_TYPE, unique: this._unique, }); - eventContext.dispatchEvent(event); + eventContext.dispatchEvent(reloadEvent); } #onProgress(event: ProgressEvent) { + event.preventDefault(); this._progress = (event.loaded / event.total) * 100; if (this._progress >= 100) { this._progress = -1; @@ -88,6 +87,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { ${when(this._progress >= 0, () => html``)} ; url?: string; status?: UmbFileDropzoneItemStatus; + /** + * The progress of the item in percentage. + */ + progress?: number; } export interface UmbEditableMediaCollectionItemModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts index 23e7f9f111..3a5e187522 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts @@ -113,8 +113,12 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { #renderPlaceholder(item: UmbMediaCollectionItemModel) { const complete = item.status === UmbFileDropzoneItemStatus.COMPLETE; + const error = item.status !== UmbFileDropzoneItemStatus.WAITING && !complete; return html` - + `; } @@ -132,10 +136,6 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { align-items: center; } - .media-placeholder-item { - font-style: italic; - } - /** TODO: Remove this fix when UUI gets upgrade to 1.3 */ umb-imaging-thumbnail { pointer-events: none; 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 24f349cec2..9b295b2455 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 @@ -112,9 +112,13 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { }; }); - this._tableColumns = [...this.#systemColumns, ...userColumns, { name: '', alias: 'entityActions' }]; + this._tableColumns = [ + ...this.#systemColumns, + ...userColumns, + { name: '', alias: 'entityActions', align: 'right' }, + ]; } else { - this._tableColumns = [...this.#systemColumns, { name: '', alias: 'entityActions' }]; + this._tableColumns = [...this.#systemColumns, { name: '', alias: 'entityActions', align: 'right' }]; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts index 5951fe8079..f1a325fd2e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts @@ -122,8 +122,6 @@ export class UmbImageCropperFocusSetterElement extends UmbLitElement { const focalPoint = { left, top } as UmbFocalPointModel; - console.log('setFocalPoint', focalPoint); - this.dispatchEvent(new UmbFocalPointChangeEvent(focalPoint)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts index 05edcf6a6b..c3f4546487 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts @@ -335,7 +335,7 @@ export class UmbImageCropperElement extends UmbLitElement { step="0.001">
- + (UmbLitElement, undefined) { @query('#dropzone') private _dropzone?: UUIFileDropzoneElement; - @property({ attribute: false }) - value: UmbImageCropperPropertyEditorValue = { - temporaryFileId: null, - src: '', - crops: [], - focalPoint: { left: 0.5, top: 0.5 }, - }; + /** + * Sets the input to required, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + required?: boolean; + + @property({ type: String }) + requiredMessage?: string; @property({ attribute: false }) crops: UmbImageCropperPropertyEditorValue['crops'] = []; @@ -40,6 +55,14 @@ export class UmbInputImageCropperElement extends UmbLitElement { constructor() { super(); this.#manager = new UmbTemporaryFileManager(this); + + this.addValidator( + 'valueMissing', + () => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, + () => { + return !!this.required && (!this.value || (this.value.src === '' && this.value.temporaryFileId == null)); + }, + ); } protected override firstUpdated(): void { @@ -54,7 +77,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { this.file = file; this.fileUnique = unique; - this.value = assignToFrozenObject(this.value, { temporaryFileId: unique }); + this.value = assignToFrozenObject(this.value ?? DefaultValue, { temporaryFileId: unique }); this.#manager?.uploadOne({ temporaryUnique: unique, file }); @@ -68,7 +91,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { } #onRemove = () => { - this.value = assignToFrozenObject(this.value, { src: '', temporaryFileId: null }); + this.value = undefined; if (this.fileUnique) { this.#manager?.removeOne(this.fileUnique); } @@ -79,25 +102,27 @@ export class UmbInputImageCropperElement extends UmbLitElement { }; #mergeCrops() { - // Replace crops from the value with the crops from the config while keeping the coordinates from the value if they exist. - const filteredCrops = this.crops.map((crop) => { - const cropFromValue = this.value.crops.find((valueCrop) => valueCrop.alias === crop.alias); - const result = { - ...crop, - coordinates: cropFromValue?.coordinates ?? undefined, + if (this.value) { + // Replace crops from the value with the crops from the config while keeping the coordinates from the value if they exist. + const filteredCrops = this.crops.map((crop) => { + const cropFromValue = this.value!.crops.find((valueCrop) => valueCrop.alias === crop.alias); + const result = { + ...crop, + coordinates: cropFromValue?.coordinates ?? undefined, + }; + + return result; + }); + + this.value = { + ...this.value, + crops: filteredCrops, }; - - return result; - }); - - this.value = { - ...this.value, - crops: filteredCrops, - }; + } } override render() { - if (this.value.src || this.file) { + if (this.value?.src || this.file) { return this.#renderImageCropper(); } @@ -116,12 +141,18 @@ export class UmbInputImageCropperElement extends UmbLitElement { const value = (e.target as UmbInputImageCropperFieldElement).value; if (!value) { - this.value = { src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, temporaryFileId: null }; + this.value = undefined; this.dispatchEvent(new UmbChangeEvent()); return; } - this.value = value; + if (this.value && this.value.temporaryFileId) { + value.temporaryFileId = this.value.temporaryFileId; + } + + if (value.temporaryFileId || value.src !== '') { + this.value = value; + } this.dispatchEvent(new UmbChangeEvent()); } 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 44378fcebe..80f62935f8 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 @@ -2,16 +2,60 @@ import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../constants.js'; import { UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js'; import type { UmbMediaItemModel } from '../../repository/item/types.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from '../../modals/index.js'; +import { UMB_MEDIA_SEARCH_PROVIDER_ALIAS } from '../../search/constants.js'; +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'; + +interface UmbMediaPickerInputContextOpenArgs { + allowedContentTypes?: Array<{ unique: string; entityType: UmbMediaTypeEntityType }>; +} export class UmbMediaPickerInputContext extends UmbPickerInputContext< UmbMediaItemModel, - UmbMediaItemModel, - UmbMediaPickerModalData, + UmbMediaTreeItemModel, + UmbMediaPickerModalData, UmbMediaPickerModalValue > { constructor(host: UmbControllerHost) { super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_PICKER_MODAL); } + + override async openPicker(pickerData?: Partial, args?: UmbMediaPickerInputContextOpenArgs) { + const combinedPickerData = { + ...pickerData, + }; + + // transform allowedContentTypes to a pickable filter + combinedPickerData.pickableFilter = (item) => this.#pickableFilter(item, args?.allowedContentTypes); + + // set default search data + if (!pickerData?.search) { + combinedPickerData.search = { + providerAlias: UMB_MEDIA_SEARCH_PROVIDER_ALIAS, + ...pickerData?.search, + }; + } + + // pass allowedContentTypes to the search request args + combinedPickerData.search!.queryParams = { + allowedContentTypes: args?.allowedContentTypes, + ...pickerData?.search?.queryParams, + }; + + super.openPicker(combinedPickerData); + } + + #pickableFilter = ( + item: UmbMediaItemModel, + allowedContentTypes?: Array<{ unique: string; entityType: UmbMediaTypeEntityType }>, + ): boolean => { + if (allowedContentTypes && allowedContentTypes.length > 0) { + return allowedContentTypes + .map((contentTypeReference) => contentTypeReference.unique) + .includes(item.mediaType.unique); + } + return true; + }; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index dc3b81e63c..919c456f17 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -1,4 +1,4 @@ -import type { UmbMediaCardItemModel, UmbMediaItemModel } from '../../types.js'; +import type { UmbMediaCardItemModel } from '../../types.js'; import { UmbMediaPickerInputContext } from './input-media.context.js'; import { css, @@ -17,6 +17,8 @@ import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/rou import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; +import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/media-type'; import '@umbraco-cms/backoffice/imaging'; @@ -111,8 +113,8 @@ export class UmbInputMediaElement extends UmbFormControlMixin { - if (this.allowedContentTypeIds && this.allowedContentTypeIds.length > 0) { - return this.allowedContentTypeIds.includes(item.mediaType.unique); - } - return true; - }; - #openPicker() { - this.#pickerContext.openPicker({ - multiple: this.max > 1, - startNode: this.startNode, - pickableFilter: this.#pickableFilter, - }); + this.#pickerContext.openPicker( + { + multiple: this.max > 1, + startNode: this.startNode, + }, + { + allowedContentTypes: this.allowedContentTypeIds?.map((id) => ({ + unique: id, + entityType: UMB_MEDIA_TYPE_ENTITY_TYPE, + })), + }, + ); } async #onRemove(item: UmbMediaCardItemModel) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts index cd9abf6633..cd4de1f9e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts @@ -1,6 +1,5 @@ -import { UmbMediaItemRepository } from '../../repository/index.js'; import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js'; -import type { UmbMediaItemModel, UmbCropModel, UmbMediaPickerPropertyValue } from '../../types.js'; +import type { UmbMediaItemModel, UmbCropModel, UmbMediaPickerPropertyValueEntry } from '../../types.js'; import type { UmbUploadableItem } from '../../dropzone/types.js'; import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { umbConfirmModal, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; @@ -9,10 +8,12 @@ import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter'; -import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; -import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository'; +import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/media'; import '@umbraco-cms/backoffice/imaging'; @@ -26,8 +27,12 @@ type UmbRichMediaCardModel = { }; @customElement('umb-input-rich-media') -export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, '') { - #sorter = new UmbSorterController(this, { +export class UmbInputRichMediaElement extends UmbFormControlMixin< + Array, + typeof UmbLitElement, + undefined +>(UmbLitElement, undefined) { + #sorter = new UmbSorterController(this, { getUniqueOfElement: (element) => { return element.id; }, @@ -37,24 +42,22 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, identifier: 'Umb.SorterIdentifier.InputRichMedia', itemSelector: 'uui-card-media', containerSelector: '.container', - //resolvePlacement: (args) => args.pointerX < args.relatedRect.left + args.relatedRect.width * 0.5, resolvePlacement: UmbSorterResolvePlacementAsGrid, onChange: ({ model }) => { - this.#items = model; - this.#sortCards(model); + this.value = model; this.dispatchEvent(new UmbChangeEvent()); }, }); - #sortCards(model: Array) { - const idToIndexMap: { [unique: string]: number } = {}; - model.forEach((item, index) => { - idToIndexMap[item.key] = index; - }); + /** + * Sets the input to required, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + required?: boolean; - const cards = [...this._cards]; - this._cards = cards.sort((a, b) => idToIndexMap[a.unique] - idToIndexMap[b.unique]); - } + @property({ type: String }) + requiredMessage?: string; /** * This is a minimum amount of selected items in this input. @@ -93,30 +96,26 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, maxMessage = 'This field exceeds the allowed amount of items'; @property({ type: Array }) - public set items(value: Array) { + public override set value(value: Array | undefined) { + super.value = value; this.#sorter.setModel(value); - this.#items = value; + this.#itemManager.setUniques(value?.map((x) => x.mediaKey)); + // Maybe the new value is using an existing media, and there we need to update the cards despite no repository update. this.#populateCards(); } - public get items(): Array { - return this.#items; + public override get value(): Array | undefined { + return super.value; } - #items: Array = []; @property({ type: Array }) allowedContentTypeIds?: string[] | undefined; - @property({ type: String }) - startNode = ''; + @property({ type: Object, attribute: false }) + startNode?: UmbTreeStartNode; @property({ type: Boolean }) multiple = false; - @property() - public override get value() { - return this.items?.map((item) => item.mediaKey).join(','); - } - @property({ type: Array }) public preselectedCrops?: Array; @@ -174,15 +173,17 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, @state() private _routeBuilder?: UmbModalRouteBuilder; - #itemRepository = new UmbMediaItemRepository(this); - - #modalManager?: UmbModalManagerContext; + readonly #itemManager = new UmbRepositoryItemsManager( + this, + UMB_MEDIA_ITEM_REPOSITORY_ALIAS, + (x) => x.unique, + ); constructor() { super(); - this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => { - this.#modalManager = instance; + this.observe(this.#itemManager.items, () => { + this.#populateCards(); }); new UmbModalRouteRegistrationController(this, UMB_IMAGE_CROPPER_EDITOR_MODAL) @@ -191,7 +192,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, const key = params.key; if (!key) return false; - const item = this.items.find((item) => item.key === key); + const item = this.value?.find((item) => item.key === key); if (!item) return false; return { @@ -212,7 +213,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, }; }) .onSubmit((value) => { - this.items = this.items.map((item) => { + this.value = this.value?.map((item) => { if (item.key !== value.key) return item; const focalPoint = this.focalPointEnabled ? value.focalPoint : null; @@ -231,15 +232,30 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, this._routeBuilder = routeBuilder; }); + this.addValidator( + 'valueMissing', + () => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, + () => { + return !this.readonly && !!this.required && (!this.value || this.value.length === 0); + }, + ); + this.addValidator( 'rangeUnderflow', () => this.minMessage, - () => !!this.min && this.items?.length < this.min, + () => + !this.readonly && + // Only if min is set: + !!this.min && + // if the value is empty and not required, we should not validate the min: + !(this.value?.length === 0 && this.required == false) && + // Validate the min: + (this.value?.length ?? 0) < this.min, ); this.addValidator( 'rangeOverflow', () => this.maxMessage, - () => !!this.max && this.items?.length > this.max, + () => !this.readonly && !!this.value && !!this.max && this.value?.length > this.max, ); } @@ -248,28 +264,29 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, } async #populateCards() { - const missingCards = this.items.filter((item) => !this._cards.find((card) => card.unique === item.key)); - if (!missingCards.length) return; + const mediaItems = this.#itemManager.getItems(); - if (!this.items?.length) { + if (!mediaItems.length) { this._cards = []; return; } + // Check if all media items is loaded. + // But notice, it would be nicer UX if we could show a loading state on the cards that are missing(loading) their items. + const missingCards = mediaItems.filter((item) => !this._cards.find((card) => card.unique === item.unique)); + const removedCards = this._cards.filter((card) => !mediaItems.find((item) => card.unique === item.unique)); + if (missingCards.length === 0 && removedCards.length === 0) return; - const uniques = this.items.map((item) => item.mediaKey); - - const { data: items } = await this.#itemRepository.requestItems(uniques); - - this._cards = this.items.map((item) => { - const media = items?.find((x) => x.unique === item.mediaKey); - return { - unique: item.key, - media: item.mediaKey, - name: media?.name ?? '', - icon: media?.mediaType?.icon, - isTrashed: media?.isTrashed ?? false, - }; - }); + this._cards = + this.value?.map((item) => { + const media = mediaItems.find((x) => x.unique === item.mediaKey); + return { + unique: item.key, + media: item.mediaKey, + name: media?.name ?? '', + icon: media?.mediaType?.icon, + isTrashed: media?.isTrashed ?? false, + }; + }) ?? []; } #pickableFilter: (item: UmbMediaItemModel) => boolean = (item) => { @@ -282,7 +299,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, #addItems(uniques: string[]) { if (!uniques.length) return; - const additions: Array = uniques.map((unique) => ({ + const additions: Array = uniques.map((unique) => ({ key: UmbId.new(), mediaKey: unique, mediaTypeAlias: '', @@ -290,12 +307,13 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, focalPoint: null, })); - this.#items = [...this.#items, ...additions]; + this.value = [...(this.value ?? []), ...additions]; this.dispatchEvent(new UmbChangeEvent()); } async #openPicker() { - const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalHandler = modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { data: { multiple: this.multiple, startNode: this.startNode, @@ -307,7 +325,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, const data = await modalHandler?.onSubmit().catch(() => null); if (!data) return; - const selection = data.selection; + const selection = data.selection.filter((x) => x !== null) as string[]; this.#addItems(selection); } @@ -319,8 +337,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, confirmLabel: this.localize.term('actions_remove'), }); - this.#items = this.#items.filter((x) => x.key !== item.unique); - this._cards = this._cards.filter((x) => x.unique !== item.unique); + this.value = this.value?.filter((x) => x.key !== item.unique); this.dispatchEvent(new UmbChangeEvent()); } @@ -356,8 +373,7 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, } #renderAddButton() { - // TODO: Stop preventing adding more, instead implement proper validation for user feedback. [NL] - if ((this._cards && this.max && this._cards.length >= this.max) || (this._cards.length && !this.multiple)) return; + if (this._cards && this._cards.length && !this.multiple) return; if (this.readonly && this._cards.length > 0) { return nothing; } else { @@ -365,6 +381,10 @@ export class UmbInputRichMediaElement extends UUIFormControlMixin(UmbLitElement, { + this.pristine = false; + this.checkValidity(); + }} @click=${this.#openPicker} label=${this.localize.term('general_choose')} ?disabled=${this.readonly}> 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 ef2d8e44df..5b8f281fe2 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 @@ -7,7 +7,9 @@ export * from './modals/constants.js'; export * from './recycle-bin/constants.js'; export * from './reference/constants.js'; export * from './repository/constants.js'; +export * from './search/constants.js'; export * from './tree/constants.js'; +export * from './url/constants.js'; export * from './workspace/constants.js'; export * from './paths.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index a598d96e60..4a1bc9e3c6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -19,7 +19,8 @@ import { UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-t import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; /** * Manages the dropzone and uploads folders and files to the server. @@ -49,9 +50,16 @@ export class UmbDropzoneManager extends UmbControllerBase { readonly #progressItems = new UmbArrayState([], (x) => x.unique); public readonly progressItems = this.#progressItems.asObservable(); + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + #localization = new UmbLocalizationController(this); + constructor(host: UmbControllerHost) { super(host); this.#host = host; + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => { + this.#notificationContext = context; + }); } public setIsFoldersAllowed(isAllowed: boolean) { @@ -70,10 +78,13 @@ export class UmbDropzoneManager extends UmbControllerBase { * Allows the user to pick a media type option if multiple types are allowed. * @param {UmbFileDropzoneDroppedItems} items - The files and folders to upload. * @param {string | null} parentUnique - Where the items should be uploaded. - * @returns {Promise>} - The items about to be uploaded. + * @returns {Array} - The items about to be uploaded. */ - public async createMediaItems(items: UmbFileDropzoneDroppedItems, parentUnique: string | null = null) { - const uploadableItems = await this.#setupProgress(items, parentUnique); + public createMediaItems(items: UmbFileDropzoneDroppedItems, parentUnique: string | null = null) { + const uploadableItems = this.#setupProgress(items, parentUnique); + + if (!uploadableItems.length) return []; + if (uploadableItems.length === 1) { // When there is only one item being uploaded, allow the user to pick the media type, if more than one is allowed. this.#createOneMediaItem(uploadableItems[0]); @@ -81,6 +92,7 @@ export class UmbDropzoneManager extends UmbControllerBase { // When there are multiple items being uploaded, automatically pick the media types for each item. We probably want to allow the user to pick the media type in the future. this.#createMediaItems(uploadableItems); } + return uploadableItems; } @@ -90,32 +102,30 @@ export class UmbDropzoneManager extends UmbControllerBase { /** * Uploads the files as temporary files and returns the data. * @param { File[] } files - The files to upload. - * @returns {Promise>} - Files as temporary files. + * @returns {Promise>} - Files as temporary files. */ - public async createTemporaryFiles(files: Array) { - const uploadableItems = (await this.#setupProgress({ files, folders: [] }, null)) as Array; + public async createTemporaryFiles(files: Array): Promise> { + const uploadableItems = this.#setupProgress({ files, folders: [] }, null) as Array; - const uploadedItems: Array = []; + const uploadedItems: Array = []; for (const item of uploadableItems) { // Upload as temp file const uploaded = await this.#tempFileManager.uploadOne({ temporaryUnique: item.temporaryFile.temporaryUnique, file: item.temporaryFile.file, + onProgress: (progress) => this.#updateProgress(item, progress), }); // Update progress - const progress = this.#progress.getValue(); - this.#progress.update({ completed: progress.completed + 1 }); - if (uploaded.status === TemporaryFileStatus.SUCCESS) { - this.#progressItems.updateOne(item.unique, { status: UmbFileDropzoneItemStatus.COMPLETE }); + this.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE); } else { - this.#progressItems.updateOne(item.unique, { status: UmbFileDropzoneItemStatus.ERROR }); + this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); } // Add to return value - uploadedItems.push(uploaded); + uploadedItems.push(item); } return uploadedItems; @@ -131,13 +141,18 @@ export class UmbDropzoneManager extends UmbControllerBase { async #createOneMediaItem(item: UmbUploadableItem) { const options = await this.#getMediaTypeOptions(item); if (!options.length) { - return this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); + this.#notificationContext?.peek('warning', { + data: { + message: `${this.#localization.term('media_disallowedFileType')}: ${item.temporaryFile?.file.name}.`, + }, + }); + return this.#updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); } const mediaTypeUnique = options.length > 1 ? await this.#showDialogMediaTypePicker(options) : options[0].unique; if (!mediaTypeUnique) { - return this.#updateProgress(item, UmbFileDropzoneItemStatus.CANCELLED); + return this.#updateStatus(item, UmbFileDropzoneItemStatus.CANCELLED); } if (item.temporaryFile) { @@ -151,7 +166,7 @@ export class UmbDropzoneManager extends UmbControllerBase { for (const item of uploadableItems) { const options = await this.#getMediaTypeOptions(item); if (!options.length) { - this.#updateProgress(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); + this.#updateStatus(item, UmbFileDropzoneItemStatus.NOT_ALLOWED); continue; } @@ -163,9 +178,9 @@ export class UmbDropzoneManager extends UmbControllerBase { // Handle files and folders differently: a file is uploaded as temp then created as a media item, and a folder is created as a media item directly if (item.temporaryFile) { - await this.#handleFile(item as UmbUploadableFile, mediaTypeUnique); + this.#handleFile(item as UmbUploadableFile, mediaTypeUnique); } else if (item.folder) { - await this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique); + this.#handleFolder(item as UmbUploadableFolder, mediaTypeUnique); } } } @@ -174,7 +189,7 @@ export class UmbDropzoneManager extends UmbControllerBase { // Upload the file as a temporary file and update progress. const temporaryFile = await this.#uploadAsTemporaryFile(item); if (temporaryFile.status !== TemporaryFileStatus.SUCCESS) { - this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); return; } @@ -183,9 +198,9 @@ export class UmbDropzoneManager extends UmbControllerBase { const { data } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique); if (data) { - this.#updateProgress(item, UmbFileDropzoneItemStatus.COMPLETE); + this.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE); } else { - this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); } } @@ -193,16 +208,17 @@ export class UmbDropzoneManager extends UmbControllerBase { const scaffold = await this.#getItemScaffold(item, mediaTypeUnique); const { data } = await this.#mediaDetailRepository.create(scaffold, item.parentUnique); if (data) { - this.#updateProgress(item, UmbFileDropzoneItemStatus.COMPLETE); + this.#updateStatus(item, UmbFileDropzoneItemStatus.COMPLETE); } else { - this.#updateProgress(item, UmbFileDropzoneItemStatus.ERROR); + this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); } } - async #uploadAsTemporaryFile(item: UmbUploadableFile) { - return await this.#tempFileManager.uploadOne({ + #uploadAsTemporaryFile(item: UmbUploadableFile) { + return this.#tempFileManager.uploadOne({ temporaryUnique: item.temporaryFile.temporaryUnique, file: item.temporaryFile.file, + onProgress: (progress) => this.#updateProgress(item, progress), }); } @@ -259,7 +275,7 @@ export class UmbDropzoneManager extends UmbControllerBase { // TODO: Use a scaffolding feature to ensure consistency. [NL] const name = item.temporaryFile ? item.temporaryFile.file.name : (item.folder?.name ?? ''); const umbracoFile: UmbMediaValueModel = { - editorAlias: null as any, + editorAlias: '', alias: 'umbracoFile', value: { temporaryFileId: item.temporaryFile?.temporaryUnique }, culture: null, @@ -277,7 +293,7 @@ export class UmbDropzoneManager extends UmbControllerBase { } // Progress handling - async #setupProgress(items: UmbFileDropzoneDroppedItems, parent: string | null) { + #setupProgress(items: UmbFileDropzoneDroppedItems, parent: string | null) { const current = this.#progress.getValue(); const currentItems = this.#progressItems.getValue(); @@ -289,12 +305,16 @@ export class UmbDropzoneManager extends UmbControllerBase { return uploadableItems; } - #updateProgress(item: UmbUploadableItem, status: UmbFileDropzoneItemStatus) { + #updateStatus(item: UmbUploadableItem, status: UmbFileDropzoneItemStatus) { this.#progressItems.updateOne(item.unique, { status }); const progress = this.#progress.getValue(); this.#progress.update({ completed: progress.completed + 1 }); } + #updateProgress(item: UmbUploadableItem, progress: number) { + this.#progressItems.updateOne(item.unique, { progress }); + } + readonly #prepareItemsAsUploadable = ( { folders, files }: UmbFileDropzoneDroppedItems, parentUnique: string | null, @@ -302,15 +322,13 @@ export class UmbDropzoneManager extends UmbControllerBase { const items: Array = []; for (const file of files) { - const unique = UmbId.new(); - if (file.type) { - items.push({ - unique, - parentUnique, - status: UmbFileDropzoneItemStatus.WAITING, - temporaryFile: { file, temporaryUnique: UmbId.new() }, - }); - } + items.push({ + unique: UmbId.new(), + parentUnique, + status: UmbFileDropzoneItemStatus.WAITING, + progress: 0, + temporaryFile: { file, temporaryUnique: UmbId.new() }, + }); } for (const subfolder of folders) { @@ -319,6 +337,7 @@ export class UmbDropzoneManager extends UmbControllerBase { unique, parentUnique, status: UmbFileDropzoneItemStatus.WAITING, + progress: 100, // Folders are created instantly. folder: { name: subfolder.folderName }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-submitted.event.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-submitted.event.ts new file mode 100644 index 0000000000..842d42a55c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-submitted.event.ts @@ -0,0 +1,15 @@ +import type { UmbUploadableItem } from './types.js'; + +export class UmbDropzoneSubmittedEvent extends Event { + public static readonly TYPE = 'submitted'; + + /** + * An array of resolved uploadable items. + */ + public items; + + public constructor(items: Array, args?: EventInit) { + super(UmbDropzoneSubmittedEvent.TYPE, { bubbles: false, composed: false, cancelable: false, ...args }); + this.items = items; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index 2805701413..f90199879d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -1,4 +1,5 @@ import { UmbDropzoneManager } from './dropzone-manager.class.js'; +import { UmbDropzoneSubmittedEvent } from './dropzone-submitted.event.js'; import { UmbFileDropzoneItemStatus, type UmbUploadableItem } from './types.js'; import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -117,10 +118,10 @@ export class UmbDropzoneElement extends UmbLitElement { if (this.createAsTemporary) { const uploadable = this.#dropzoneManager.createTemporaryFiles(event.detail.files); - this.dispatchEvent(new CustomEvent('submitted', { detail: await uploadable })); + this.dispatchEvent(new UmbDropzoneSubmittedEvent(await uploadable)); } else { const uploadable = this.#dropzoneManager.createMediaItems(event.detail, this.parentUnique); - this.dispatchEvent(new CustomEvent('submitted', { detail: await uploadable })); + this.dispatchEvent(new UmbDropzoneSubmittedEvent(uploadable)); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts index 38624dcb34..c965489395 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts @@ -1,2 +1,3 @@ export * from './dropzone.element.js'; export * from './dropzone-manager.class.js'; +export * from './dropzone-submitted.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts index 0e99dbb2d7..f1b90a32e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts @@ -11,6 +11,7 @@ export interface UmbUploadableItem { unique: string; parentUnique: string | null; status: UmbFileDropzoneItemStatus; + progress: number; folder?: { name: string }; temporaryFile?: UmbTemporaryFileModel; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts index 377c05b6dc..f9957ebb0d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts @@ -1,9 +1,10 @@ export * from './components/index.js'; -export * from './dropzone/index.js'; export * from './constants.js'; +export * from './dropzone/index.js'; export * from './reference/index.js'; export * from './repository/index.js'; export * from './search/index.js'; +export * from './url/index.js'; export * from './utils/index.js'; export { UmbMediaAuditLogRepository } from './audit-log/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts index d6f8ad84e4..1b61cda607 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts @@ -1,31 +1,37 @@ +import { manifests as auditLogManifests } from './audit-log/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as dropzoneManifests } from './dropzone/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionsManifests } from './entity-bulk-actions/manifests.js'; +import { manifests as fileUploadPreviewManifests } from './components/input-upload-field/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as propertyEditorsManifests } from './property-editors/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; +import { manifests as referenceManifests } from './reference/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as searchManifests } from './search/manifests.js'; import { manifests as sectionViewManifests } from './dashboard/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; +import { manifests as urlManifests } from './url/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; -import { manifests as fileUploadPreviewManifests } from './components/input-upload-field/manifests.js'; export const manifests: Array = [ + ...auditLogManifests, ...collectionManifests, ...dropzoneManifests, ...entityActionsManifests, ...entityBulkActionsManifests, + ...fileUploadPreviewManifests, ...menuManifests, ...modalManifests, ...propertyEditorsManifests, ...recycleBinManifests, + ...referenceManifests, ...repositoryManifests, ...searchManifests, ...sectionViewManifests, ...treeManifests, + ...urlManifests, ...workspaceManifests, - ...fileUploadPreviewManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/image-cropper-editor/image-cropper-editor-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/image-cropper-editor/image-cropper-editor-modal.element.ts index 33a7bbfa08..f66865668b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/image-cropper-editor/image-cropper-editor-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/image-cropper-editor/image-cropper-editor-modal.element.ts @@ -1,8 +1,8 @@ -import { UmbMediaUrlRepository } from '../../repository/index.js'; import { UMB_MEDIA_PICKER_MODAL } from '../media-picker/media-picker-modal.token.js'; import type { UmbCropModel } from '../../types.js'; import type { UmbInputImageCropperFieldElement } from '../../components/input-image-cropper/image-cropper-field.element.js'; import type { UmbImageCropperPropertyEditorValue } from '../../components/index.js'; +import { UmbMediaUrlRepository } from '../../url/index.js'; import type { UmbImageCropperEditorModalData, UmbImageCropperEditorModalValue, @@ -108,7 +108,14 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement< const data = await modal?.onSubmit().catch(() => null); if (!data) return; - this._unique = data.selection[0]; + const selected = data.selection[0]; + + if (!selected) { + throw new Error('No media selected'); + } + + this._unique = selected; + this.value = { ...this.value, unique: this._unique }; this.#getSrc(); } 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 6508d21c7a..ab8a431de5 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 @@ -31,10 +31,7 @@ const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_M // TODO: investigate how we can reuse the picker-search-field element, picker context etc. @customElement('umb-media-picker-modal') -export class UmbMediaPickerModalElement extends UmbModalBaseElement< - UmbMediaPickerModalData, - UmbMediaPickerModalValue -> { +export class UmbMediaPickerModalElement extends UmbModalBaseElement { #mediaTreeRepository = new UmbMediaTreeRepository(this); #mediaItemRepository = new UmbMediaItemRepository(this); #mediaSearchProvider = new UmbMediaSearchProvider(this); @@ -91,8 +88,10 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement< protected override async firstUpdated(_changedProperties: PropertyValues): Promise { super.firstUpdated(_changedProperties); - if (this.data?.startNode) { - const { data } = await this.#mediaItemRepository.requestItems([this.data.startNode]); + const startNode = this.data?.startNode; + + if (startNode) { + const { data } = await this.#mediaItemRepository.requestItems([startNode.unique]); this._startNode = data?.[0]; if (this._startNode) { @@ -165,7 +164,11 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement< } const query = this._searchQuery; - const { data } = await this.#mediaSearchProvider.search({ query, searchFrom: this._searchFrom }); + const { data } = await this.#mediaSearchProvider.search({ + query, + searchFrom: this._searchFrom, + ...this.data?.search?.queryParams, + }); if (!data) { // No search results. @@ -229,12 +232,12 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement< override render() { return html` - + ${this.#renderBody()} ${this.#renderBreadcrumb()}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts index 3e7cc80348..aebece4e35 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts @@ -1,17 +1,11 @@ -import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import type { UmbMediaItemModel } from '../../repository/types.js'; +import type { UmbTreePickerModalData } from '@umbraco-cms/backoffice/tree'; +import { UmbModalToken, type UmbPickerModalValue } from '@umbraco-cms/backoffice/modal'; -export interface UmbMediaPickerModalData { - startNode?: string | null; - multiple?: boolean; - pickableFilter?: (item: ItemType) => boolean; - filter?: (item: ItemType) => boolean; -} +export type UmbMediaPickerModalData = UmbTreePickerModalData; +export type UmbMediaPickerModalValue = UmbPickerModalValue; -export interface UmbMediaPickerModalValue { - selection: string[]; -} - -export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken, UmbMediaPickerModalValue>( +export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken( 'Umb.Modal.MediaPicker', { modal: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts index b7f7ea2acc..dc2eed16e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts @@ -1,59 +1,62 @@ import type { UmbImageCropperPropertyEditorValue, UmbInputImageCropperElement } from '../../components/index.js'; -import { html, customElement, property, nothing, state } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { type UmbPropertyEditorUiElement, UmbPropertyValueChangeEvent, type UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + import '../../components/input-image-cropper/input-image-cropper.element.js'; /** * @element umb-property-editor-ui-image-cropper */ @customElement('umb-property-editor-ui-image-cropper') -export class UmbPropertyEditorUIImageCropperElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property({ attribute: false }) - value: UmbImageCropperPropertyEditorValue = { - temporaryFileId: null, - src: '', - crops: [], - focalPoint: { left: 0.5, top: 0.5 }, - }; +export class UmbPropertyEditorUIImageCropperElement + extends UmbFormControlMixin( + UmbLitElement, + ) + implements UmbPropertyEditorUiElement +{ + /** + * Sets the input to mandatory, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + mandatory?: boolean; + + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; @state() crops: UmbImageCropperPropertyEditorValue['crops'] = []; - override updated(changedProperties: Map) { - super.updated(changedProperties); - if (changedProperties.has('value')) { - if (!this.value) { - this.value = { - temporaryFileId: null, - src: '', - crops: [], - focalPoint: { left: 0.5, top: 0.5 }, - }; - } - } - } - public set config(config: UmbPropertyEditorConfigCollection | undefined) { this.crops = config?.getValueByAlias('crops') ?? []; } + override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-image-cropper')!); + } + + override focus() { + return this.shadowRoot?.querySelector('umb-input-image-cropper')?.focus(); + } + #onChange(e: Event) { this.value = (e.target as UmbInputImageCropperElement).value; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } override render() { - if (!this.value) return nothing; - return html``; + .crops=${this.crops} + .required=${this.mandatory} + .requiredMessage=${this.mandatoryMessage}>`; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts index 281d3a47b8..2ff636281a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts @@ -1,5 +1,6 @@ import type { UmbInputRichMediaElement } from '../../components/input-rich-media/input-rich-media.element.js'; -import type { UmbCropModel, UmbMediaPickerPropertyValue } from '../types.js'; +import type { UmbCropModel, UmbMediaPickerValueModel } from '../types.js'; +import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; @@ -9,6 +10,8 @@ import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import '../../components/input-rich-media/input-rich-media.element.js'; @@ -18,10 +21,10 @@ const elementName = 'umb-property-editor-ui-media-picker'; * @element umb-property-editor-ui-media-picker */ @customElement(elementName) -export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property({ attribute: false }) - value?: Array; - +export class UmbPropertyEditorUIMediaPickerElement + extends UmbFormControlMixin(UmbLitElement) + implements UmbPropertyEditorUiElement +{ public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; @@ -29,13 +32,25 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme this._focalPointEnabled = Boolean(config.getValueByAlias('enableLocalFocalPoint')); this._multiple = Boolean(config.getValueByAlias('multiple')); this._preselectedCrops = config?.getValueByAlias>('crops') ?? []; - this._startNode = config.getValueByAlias('startNodeId') ?? ''; + + const startNodeId = config.getValueByAlias('startNodeId') ?? ''; + this._startNode = startNodeId ? { unique: startNodeId, entityType: UMB_MEDIA_ENTITY_TYPE } : undefined; const minMax = config.getValueByAlias('validationLimit'); this._min = minMax?.min ?? 0; this._max = minMax?.max ?? Infinity; } + /** + * Sets the input to mandatory, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + mandatory?: boolean; + + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; + /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. * @type {boolean} @@ -46,7 +61,7 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme readonly = false; @state() - private _startNode: string = ''; + private _startNode?: UmbTreeStartNode; @state() private _focalPointEnabled: boolean = false; @@ -81,8 +96,17 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme }); } + override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-rich-media')!); + } + + override focus() { + return this.shadowRoot?.querySelector('umb-input-rich-media')?.focus(); + } + #onChange(event: CustomEvent & { target: UmbInputRichMediaElement }) { - this.value = event.target.items; + const isEmpty = event.target.value?.length === 0; + this.value = isEmpty ? undefined : event.target.value; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } @@ -92,12 +116,14 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme .alias=${this._alias} .allowedContentTypeIds=${this._allowedMediaTypes} .focalPointEnabled=${this._focalPointEnabled} - .items=${this.value ?? []} + .value=${this.value ?? []} .max=${this._max} .min=${this._min} .preselectedCrops=${this._preselectedCrops} .startNode=${this._startNode} .variantId=${this._variantId} + .required=${this.mandatory} + .requiredMessage=${this.mandatoryMessage} ?multiple=${this._multiple} @change=${this.#onChange} ?readonly=${this.readonly}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/types.ts index 0baa97364e..85109d3b08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/types.ts @@ -1,4 +1,4 @@ -export type UmbMediaPickerPropertyValue = { +export type UmbMediaPickerPropertyValueEntry = { key: string; mediaKey: string; mediaTypeAlias: string; @@ -6,6 +6,14 @@ export type UmbMediaPickerPropertyValue = { crops: Array; }; +/** + * @deprecated Use UmbMediaPickerPropertyValueEntry instead — Will be removed in v.17. + * Also notice this is a modal for the entry type, use UmbMediaPickerPropertyValueModel for the type of the value. + */ +export type UmbMediaPickerPropertyValue = UmbMediaPickerPropertyValueEntry; + +export type UmbMediaPickerValueModel = Array; + export type UmbCropModel = { label?: string; alias: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.repository.ts index 8020473d47..a15f5cd2ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.repository.ts @@ -21,7 +21,7 @@ export class UmbMediaRecycleBinTreeRepository const data = { unique: null, entityType: UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE, - name: 'Recycle Bin', + name: '#treeHeaders_contentRecycleBin', icon: 'icon-trash', hasChildren, isContainer: false, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/info-app/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/info-app/manifests.ts new file mode 100644 index 0000000000..6740bdc27f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/info-app/manifests.ts @@ -0,0 +1,18 @@ +import { UMB_MEDIA_WORKSPACE_ALIAS } from '../../workspace/constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceInfoApp', + name: 'Media References Workspace Info App', + alias: 'Umb.WorkspaceInfoApp.Media.References', + element: () => import('./media-references-workspace-info-app.element.js'), + weight: 90, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_MEDIA_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info-reference.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/info-app/media-references-workspace-info-app.element.ts similarity index 78% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info-reference.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/reference/info-app/media-references-workspace-info-app.element.ts index 4f9aaa6bb8..e552b85540 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info-reference.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/info-app/media-references-workspace-info-app.element.ts @@ -1,5 +1,6 @@ -import { UmbMediaReferenceRepository } from '../../../reference/index.js'; -import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbMediaReferenceRepository } from '../repository/index.js'; +import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../workspace/constants.js'; +import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { isDefaultReference, isDocumentReference, isMediaReference } from '@umbraco-cms/backoffice/relations'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; @@ -8,18 +9,16 @@ import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import type { UmbReferenceModel } from '@umbraco-cms/backoffice/relations'; import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; -@customElement('umb-media-workspace-view-info-reference') -export class UmbMediaWorkspaceViewInfoReferenceElement extends UmbLitElement { +@customElement('umb-media-references-workspace-info-app') +export class UmbMediaReferencesWorkspaceInfoAppElement extends UmbLitElement { #itemsPerPage = 10; #referenceRepository; #routeBuilder?: UmbModalRouteBuilder; - @property() - mediaUnique = ''; - @state() private _currentPage = 1; @@ -32,6 +31,9 @@ export class UmbMediaWorkspaceViewInfoReferenceElement extends UmbLitElement { @state() private _loading = true; + #workspaceContext?: typeof UMB_MEDIA_WORKSPACE_CONTEXT.TYPE; + #mediaUnique?: UmbEntityUnique; + constructor() { super(); this.#referenceRepository = new UmbMediaReferenceRepository(this); @@ -44,6 +46,32 @@ export class UmbMediaWorkspaceViewInfoReferenceElement extends UmbLitElement { .observeRouteBuilder((routeBuilder) => { this.#routeBuilder = routeBuilder; }); + + this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; + this.#observeMediaUnique(); + }); + } + + #observeMediaUnique() { + this.observe( + this.#workspaceContext?.unique, + (unique) => { + if (!unique) { + this.#mediaUnique = undefined; + this._items = []; + return; + } + + if (this.#mediaUnique === unique) { + return; + } + + this.#mediaUnique = unique; + this.#getReferences(); + }, + 'umbReferencesDocumentUniqueObserver', + ); } protected override firstUpdated(): void { @@ -51,10 +79,14 @@ export class UmbMediaWorkspaceViewInfoReferenceElement extends UmbLitElement { } async #getReferences() { + if (!this.#mediaUnique) { + throw new Error('Media unique is required'); + } + this._loading = true; const { data } = await this.#referenceRepository.requestReferencedBy( - this.mediaUnique, + this.#mediaUnique, (this._currentPage - 1) * this.#itemsPerPage, this.#itemsPerPage, ); @@ -123,22 +155,20 @@ export class UmbMediaWorkspaceViewInfoReferenceElement extends UmbLitElement { } override render() { + if (!this._items?.length) return nothing; return html` - + ${when( this._loading, () => html``, () => html`${this.#renderItems()} ${this.#renderPagination()}`, )} - + `; } #renderItems() { - if (!this._items?.length) - return html`

- This item has no references. -

`; + if (!this._items?.length) return nothing; return html` @@ -218,10 +248,10 @@ export class UmbMediaWorkspaceViewInfoReferenceElement extends UmbLitElement { ]; } -export default UmbMediaWorkspaceViewInfoReferenceElement; +export default UmbMediaReferencesWorkspaceInfoAppElement; declare global { interface HTMLElementTagNameMap { - 'umb-media-workspace-view-info-reference': UmbMediaWorkspaceViewInfoReferenceElement; + 'umb-media-references-workspace-info-app': UmbMediaReferencesWorkspaceInfoAppElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/manifests.ts index 4ac6fbdcb2..cad6350ec8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/manifests.ts @@ -1,3 +1,4 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as infoAppManifests } from './info-app/manifests.js'; -export const manifests: Array = [...repositoryManifests]; +export const manifests: Array = [...repositoryManifests, ...infoAppManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/constants.ts index 655e81e66d..7a6c4a9d9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/constants.ts @@ -1,4 +1,3 @@ export * from './detail/constants.js'; export * from './item/constants.js'; -export * from './url/constants.js'; export * from './validation/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts index 034f9d529b..d55093c761 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts @@ -1,3 +1,2 @@ export { UmbMediaDetailRepository } from './detail/index.js'; export { UmbMediaItemRepository } from './item/index.js'; -export { UmbMediaUrlRepository } from './url/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts index 62a6814b70..74a3121896 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts @@ -1,11 +1,5 @@ import { manifests as detailManifests } from './detail/manifests.js'; import { manifests as itemManifests } from './item/manifests.js'; -import { manifests as urlManifests } from './url/manifests.js'; import { manifests as validationManifests } from './validation/manifests.js'; -export const manifests: Array = [ - ...detailManifests, - ...itemManifests, - ...urlManifests, - ...validationManifests, -]; +export const manifests: Array = [...detailManifests, ...itemManifests, ...validationManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/types.ts index 6988c782f6..3c4135818c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/types.ts @@ -1,2 +1 @@ -export type { UmbMediaUrlModel } from './url/types.js'; export type { UmbMediaItemModel } from './item/types.js'; 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 new file mode 100644 index 0000000000..8a888cf4ac --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/constants.ts @@ -0,0 +1 @@ +export const UMB_MEDIA_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Media'; 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 73a1508bf8..82e9d81d68 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,9 +1,10 @@ import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; +import { UMB_MEDIA_SEARCH_PROVIDER_ALIAS } from './constants.js'; export const manifests: Array = [ { name: 'Media Search Provider', - alias: 'Umb.SearchProvider.Media', + alias: UMB_MEDIA_SEARCH_PROVIDER_ALIAS, type: 'searchProvider', api: () => import('./media.search-provider.js'), weight: 700, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media-search.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media-search.repository.ts index d47162bcbe..407dd494be 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media-search.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media-search.repository.ts @@ -1,13 +1,13 @@ import { UmbMediaSearchServerDataSource } from './media-search.server.data-source.js'; -import type { UmbMediaSearchItemModel } from './types.js'; -import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbMediaSearchItemModel, UmbMediaSearchRequestArgs } from './types.js'; +import type { UmbSearchRepository } from '@umbraco-cms/backoffice/search'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; export class UmbMediaSearchRepository extends UmbControllerBase - implements UmbSearchRepository, UmbApi + implements UmbSearchRepository, UmbApi { #dataSource: UmbMediaSearchServerDataSource; @@ -17,7 +17,7 @@ export class UmbMediaSearchRepository this.#dataSource = new UmbMediaSearchServerDataSource(this); } - search(args: UmbSearchRequestArgs) { + search(args: UmbMediaSearchRequestArgs) { return this.#dataSource.search(args); } } 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 5edc2a2737..207406449d 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 @@ -1,6 +1,6 @@ import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; -import type { UmbMediaSearchItemModel } from './types.js'; -import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbMediaSearchItemModel, UmbMediaSearchRequestArgs } from './types.js'; +import type { UmbSearchDataSource } from '@umbraco-cms/backoffice/search'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { MediaService } from '@umbraco-cms/backoffice/external/backend-api'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -10,7 +10,9 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbMediaSearchServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbMediaSearchServerDataSource implements UmbSearchDataSource { +export class UmbMediaSearchServerDataSource + implements UmbSearchDataSource +{ #host: UmbControllerHost; /** @@ -24,16 +26,17 @@ export class UmbMediaSearchServerDataSource implements UmbSearchDataSource mediaReference.unique), }), ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media.search-provider.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media.search-provider.ts index fa9439674e..bdf2ca08ac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media.search-provider.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/media.search-provider.ts @@ -1,12 +1,15 @@ import { UmbMediaSearchRepository } from './media-search.repository.js'; -import type { UmbMediaSearchItemModel } from './types.js'; -import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbMediaSearchItemModel, UmbMediaSearchRequestArgs } from './types.js'; +import type { UmbSearchProvider } from '@umbraco-cms/backoffice/search'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -export class UmbMediaSearchProvider extends UmbControllerBase implements UmbSearchProvider { +export class UmbMediaSearchProvider + extends UmbControllerBase + implements UmbSearchProvider +{ #repository = new UmbMediaSearchRepository(this); - async search(args: UmbSearchRequestArgs) { + async search(args: UmbMediaSearchRequestArgs) { return this.#repository.search(args); } 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 610c12cfb4..246505347e 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 @@ -1,5 +1,11 @@ import type { UmbMediaItemModel } from '../types.js'; +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbMediaTypeEntityType } from '@umbraco-cms/backoffice/media-type'; export interface UmbMediaSearchItemModel extends UmbMediaItemModel { href: string; } + +export interface UmbMediaSearchRequestArgs extends UmbSearchRequestArgs { + allowedContentTypes?: Array<{ unique: string; entityType: UmbMediaTypeEntityType }>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts index ade991b00f..c9545de5a0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts @@ -6,11 +6,12 @@ import type { UmbContentDetailModel, UmbElementValueModel } from '@umbraco-cms/b export type * from './audit-log/types.js'; export type * from './collection/types.js'; export type * from './dropzone/types.js'; -export type * from './recycle-bin/types.js'; export type * from './modals/types.js'; +export type * from './recycle-bin/types.js'; export type * from './repository/types.js'; export type * from './search/types.js'; export type * from './tree/types.js'; +export type * from './url/types.js'; export interface UmbMediaDetailModel extends UmbContentDetailModel { mediaType: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/url/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/constants.ts new file mode 100644 index 0000000000..41a409dec1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/constants.ts @@ -0,0 +1 @@ +export * from './repository/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/url/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/index.ts new file mode 100644 index 0000000000..3d76f338dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/index.ts @@ -0,0 +1 @@ +export * from './repository/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/url/info-app/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/info-app/manifests.ts new file mode 100644 index 0000000000..ac663b2e9c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/info-app/manifests.ts @@ -0,0 +1,18 @@ +import { UMB_MEDIA_WORKSPACE_ALIAS } from '../../workspace/constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceInfoApp', + name: 'Media Links Workspace Info App', + alias: 'Umb.WorkspaceInfoApp.Media.Links', + element: () => import('./media-links-workspace-info-app.element.js'), + weight: 100, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_MEDIA_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/url/info-app/media-links-workspace-info-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/info-app/media-links-workspace-info-app.element.ts new file mode 100644 index 0000000000..114792debb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/info-app/media-links-workspace-info-app.element.ts @@ -0,0 +1,144 @@ +import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../workspace/constants.js'; +import type { MediaUrlInfoModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { css, customElement, html, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-media-links-workspace-info-app') +export class UmbMediaLinksWorkspaceInfoAppElement extends UmbLitElement { + @state() + private _urls?: Array; + + #workspaceContext?: typeof UMB_MEDIA_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; + this.#observeUrls(); + }); + } + + #observeUrls() { + if (!this.#workspaceContext) return; + + this.observe( + this.#workspaceContext.urls, + (urls) => { + this._urls = urls; + }, + '__urls', + ); + } + + protected override render() { + return html` + ${this.#renderLinksSection()} + `; + } + + #openSvg(imagePath: string) { + const popup = window.open('', '_blank'); + if (!popup) return; + + const html = ` + + + +`; + + popup.document.open(); + popup.document.write(html); + popup.document.close(); + } + + #renderLinksSection() { + if (this._urls && this._urls.length) { + return html` + ${repeat( + this._urls, + (item) => item.url, + (item) => this.#renderLinkItem(item), + )} + `; + } else { + return html` + + `; + } + } + + #renderLinkItem(item: MediaUrlInfoModel) { + const ext = item.url.split(/[#?]/)[0].split('.').pop()?.trim(); + if (ext === 'svg') { + return html` + this.#openSvg(item.url)}> + ${item.url} + + + `; + } else { + return html` + + ${item.url} + + + `; + } + } + + static override styles = [ + css` + uui-box { + --uui-box-default-padding: 0; + } + + #link-section { + display: flex; + flex-direction: column; + text-align: left; + } + + .link-item { + padding: var(--uui-size-space-4) var(--uui-size-space-5); + display: grid; + grid-template-columns: 1fr auto; + gap: var(--uui-size-6); + color: inherit; + text-decoration: none; + word-break: break-all; + } + + .link-language { + color: var(--uui-color-divider-emphasis); + } + + .link-content.italic { + font-style: italic; + } + + .link-item uui-icon { + margin-right: var(--uui-size-space-2); + vertical-align: middle; + } + + .link-item.with-href { + cursor: pointer; + } + + .link-item.with-href:hover { + background: var(--uui-color-divider); + } + `, + ]; +} + +export default UmbMediaLinksWorkspaceInfoAppElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-links-workspace-info-app': UmbMediaLinksWorkspaceInfoAppElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/url/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/manifests.ts new file mode 100644 index 0000000000..cad6350ec8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as infoAppManifests } from './info-app/manifests.js'; + +export const manifests: Array = [...repositoryManifests, ...infoAppManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.repository.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.repository.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.server.data-source.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.server.data-source.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.store.context-token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.context-token.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.store.context-token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.store.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/media-url.store.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/types.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/url/repository/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/url/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/types.ts new file mode 100644 index 0000000000..e32ac4b889 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/url/types.ts @@ -0,0 +1 @@ +export type * from './repository/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts index c79e95d0f4..4dce9bd32a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts @@ -1,21 +1,16 @@ import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../media-workspace.context-token.js'; -import { TimeOptions } from './utils.js'; -import { css, customElement, html, ifDefined, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { TimeOptions } from '../../../audit-log/info-app/utils.js'; +import { css, customElement, html, ifDefined, nothing, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbMediaTypeItemModel } from '@umbraco-cms/backoffice/media-type'; import { UMB_MEDIA_TYPE_ENTITY_TYPE, UmbMediaTypeItemRepository } from '@umbraco-cms/backoffice/media-type'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; -import type { MediaUrlInfoModel } from '@umbraco-cms/backoffice/external/backend-api'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; -// import of local components -import './media-workspace-view-info-history.element.js'; -import './media-workspace-view-info-reference.element.js'; - @customElement('umb-media-workspace-view-info') export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { @state() @@ -37,9 +32,6 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { #mediaTypeItemRepository = new UmbMediaTypeItemRepository(this); - @state() - private _urls?: Array; - @state() private _createDate?: string | null = null; @@ -90,14 +82,6 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { #observeContent() { if (!this.#workspaceContext) return; - this.observe( - this.#workspaceContext.urls, - (urls) => { - this._urls = urls; - }, - '__urls', - ); - this.observe( this.#workspaceContext.unique, (unique) => { @@ -112,33 +96,11 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { this._updateDate = variants?.[0]?.updateDate; }); } - #openSvg(imagePath: string) { - const popup = window.open('', '_blank'); - if (!popup) return; - - const html = ` - - - -`; - - popup.document.open(); - popup.document.write(html); - popup.document.close(); - } override render() { return html`
- - - - - - - +
item.url, - (item) => this.#renderLinkItem(item), - )} - `; - } else { - return html` - - `; - } - } - - #renderLinkItem(item: MediaUrlInfoModel) { - const ext = item.url.split(/[#?]/)[0].split('.').pop()?.trim(); - if (ext === 'svg') { - return html` - this.#openSvg(item.url)}> - ${item.url} - - - `; - } else { - return html` - - ${item.url} - - - `; - } - } - #renderGeneralSection() { return html` ${this.#renderCreateDate()} ${this.#renderUpdateDate()} @@ -264,44 +189,6 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { margin-bottom: var(--uui-size-space-6); } - // Link section - - #link-section { - display: flex; - flex-direction: column; - text-align: left; - } - - .link-item { - padding: var(--uui-size-space-4) var(--uui-size-space-6); - display: grid; - grid-template-columns: 1fr auto; - gap: var(--uui-size-6); - color: inherit; - text-decoration: none; - } - - .link-language { - color: var(--uui-color-divider-emphasis); - } - - .link-content.italic { - font-style: italic; - } - - .link-item uui-icon { - margin-right: var(--uui-size-space-2); - vertical-align: middle; - } - - .link-item.with-href { - cursor: pointer; - } - - .link-item.with-href:hover { - background: var(--uui-color-divider); - } - uui-ref-node-document-type[readonly] { padding-top: 7px; padding-bottom: 7px; 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 a3ca46c992..95e1e3399f 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 @@ -22,6 +22,7 @@ export class UmbMemberGroupTableCollectionViewElement extends UmbLitElement { { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.element.ts index 1bac55d721..bf2689355e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.element.ts @@ -49,7 +49,7 @@ export class UmbMemberGroupPickerModalElement extends UmbModalBaseElement< } override render() { - return html` + return html` ${repeat( this.#filteredMemberGroups, @@ -67,8 +67,12 @@ export class UmbMemberGroupPickerModalElement extends UmbModalBaseElement< )}
- - + +
`; } 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 d7f0b3abcf..02f8cb5643 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 @@ -40,6 +40,7 @@ export class UmbMemberTableCollectionViewElement extends UmbLitElement { { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.context.ts index ca77c272ac..b8c77827b9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.context.ts @@ -1,13 +1,67 @@ import type { UmbMemberItemModel } from '../../repository/index.js'; -import { UMB_MEMBER_PICKER_MODAL } from '../member-picker-modal/member-picker-modal.token.js'; -import { UMB_MEMBER_ITEM_REPOSITORY_ALIAS } from '../../constants.js'; +import { + UMB_MEMBER_PICKER_MODAL, + type UmbMemberPickerModalData, + type UmbMemberPickerModalValue, +} from '../member-picker-modal/member-picker-modal.token.js'; +import { UMB_MEMBER_ITEM_REPOSITORY_ALIAS, UMB_MEMBER_SEARCH_PROVIDER_ALIAS } from '../../constants.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbMemberTypeEntityType } from '@umbraco-cms/backoffice/member-type'; -export class UmbMemberPickerInputContext extends UmbPickerInputContext { +interface UmbMemberPickerInputContextOpenArgs { + allowedContentTypes?: Array<{ unique: string; entityType: UmbMemberTypeEntityType }>; +} + +export class UmbMemberPickerInputContext extends UmbPickerInputContext< + UmbMemberItemModel, + UmbMemberItemModel, + UmbMemberPickerModalData, + UmbMemberPickerModalValue +> { constructor(host: UmbControllerHostElement) { super(host, UMB_MEMBER_ITEM_REPOSITORY_ALIAS, UMB_MEMBER_PICKER_MODAL); } + + override async openPicker( + pickerData?: Partial, + args?: UmbMemberPickerInputContextOpenArgs, + ) { + const combinedPickerData = { + ...pickerData, + }; + + // transform allowedContentTypes to a pickable filter + combinedPickerData.pickableFilter = (item) => this.#pickableFilter(item, args?.allowedContentTypes); + + // set default search data + if (!pickerData?.search) { + combinedPickerData.search = { + providerAlias: UMB_MEMBER_SEARCH_PROVIDER_ALIAS, + ...pickerData?.search, + }; + } + + // pass allowedContentTypes to the search request args + combinedPickerData.search!.queryParams = { + allowedContentTypes: args?.allowedContentTypes, + ...pickerData?.search?.queryParams, + }; + + super.openPicker(combinedPickerData); + } + + #pickableFilter = ( + item: UmbMemberItemModel, + allowedContentTypes?: Array<{ unique: string; entityType: UmbMemberTypeEntityType }>, + ): boolean => { + if (allowedContentTypes && allowedContentTypes.length > 0) { + return allowedContentTypes + .map((contentTypeReference) => contentTypeReference.unique) + .includes(item.memberType.unique); + } + return true; + }; } /** @deprecated Use `UmbMemberPickerInputContext` instead. This method will be removed in Umbraco 15. */ diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts index 9f782e9ea9..0186182e63 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts @@ -8,6 +8,7 @@ import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/member-type'; const elementName = 'umb-input-member'; @@ -159,18 +160,18 @@ export class UmbInputMemberElement extends UmbFormControlMixin (this._items = selectedItems), '_observeItems'); } - #pickableFilter = (item: UmbMemberItemModel): boolean => { - if (this.allowedContentTypeIds && this.allowedContentTypeIds.length > 0) { - return this.allowedContentTypeIds.includes(item.memberType.unique); - } - return true; - }; - #openPicker() { - this.#pickerContext.openPicker({ - filter: this.filter, - pickableFilter: this.#pickableFilter, - }); + this.#pickerContext.openPicker( + { + filter: this.filter, + }, + { + allowedContentTypes: this.allowedContentTypeIds?.map((id) => ({ + unique: id, + entityType: UMB_MEMBER_TYPE_ENTITY_TYPE, + })), + }, + ); } #onRemove(item: UmbMemberItemModel) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts index 1ed25c4d41..89571c3eaf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts @@ -14,11 +14,14 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< UmbMemberPickerModalValue > { @state() - private _members: Array = []; + private _members: Array = []; @state() private _searchQuery?: string; + @state() + private _selectableFilter: (item: UmbMemberItemModel) => boolean = () => true; + #collectionRepository = new UmbMemberCollectionRepository(this); #pickerContext = new UmbCollectionItemPickerContext(this); @@ -46,8 +49,24 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< super.updated(_changedProperties); if (_changedProperties.has('data')) { - this.#pickerContext.search.updateConfig({ ...this.data?.search }); this.#pickerContext.selection.setMultiple(this.data?.multiple ?? false); + + if (this.data?.pickableFilter) { + this._selectableFilter = this.data?.pickableFilter; + } + + if (this.data?.search) { + this.#pickerContext.search.updateConfig({ + ...this.data.search, + }); + + const searchQueryParams = this.data.search.queryParams; + if (searchQueryParams) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore - TODO wire up types + this.#pickerContext.search.setQuery(searchQueryParams); + } + } } if (_changedProperties.has('value')) { @@ -96,10 +115,15 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< } #renderMemberItem(item: UmbMemberItemModel | UmbMemberDetailModel) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - TODO: MemberDetailModel does not have a name. It should have so we ignore this for now. + const selectable = this._selectableFilter(item); + return html` this.#pickerContext.selection.select(item.unique)} @deselected=${() => this.#pickerContext.selection.deselect(item.unique)} ?selected=${this.#pickerContext.selection.isSelected(item.unique)}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts index 8da183eefe..a49bce2f61 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts @@ -1,17 +1,10 @@ import type { UmbMemberItemModel } from '../../repository/index.js'; import { UMB_MEMBER_SEARCH_PROVIDER_ALIAS } from '../../search/constants.js'; -import type { UmbPickerModalSearchConfig } from '@umbraco-cms/backoffice/modal'; +import type { UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -export interface UmbMemberPickerModalData { - multiple?: boolean; - filter?: (member: UmbMemberItemModel) => boolean; - search?: UmbPickerModalSearchConfig; -} - -export interface UmbMemberPickerModalValue { - selection: Array; -} +export type UmbMemberPickerModalData = UmbPickerModalData; +export type UmbMemberPickerModalValue = UmbPickerModalValue; export const UMB_MEMBER_PICKER_MODAL = new UmbModalToken( 'Umb.Modal.MemberPicker', diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member-search.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member-search.repository.ts index 67e270b90d..8e0e4e884c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member-search.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member-search.repository.ts @@ -1,5 +1,5 @@ import { UmbMemberSearchServerDataSource } from './member-search.server.data-source.js'; -import type { UmbMemberSearchItemModel } from './member.search-provider.js'; +import type { UmbMemberSearchItemModel } from './types.js'; import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member-search.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member-search.server.data-source.ts index febcdd024c..6a95cef431 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member-search.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member-search.server.data-source.ts @@ -1,6 +1,6 @@ import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js'; -import type { UmbMemberSearchItemModel } from './member.search-provider.js'; -import type { UmbSearchDataSource, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbMemberSearchItemModel, UmbMemberSearchRequestArgs } from './types.js'; +import type { UmbSearchDataSource } from '@umbraco-cms/backoffice/search'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { MemberService } from '@umbraco-cms/backoffice/external/backend-api'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; @@ -10,7 +10,9 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; * @class UmbMemberSearchServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbMemberSearchServerDataSource implements UmbSearchDataSource { +export class UmbMemberSearchServerDataSource + implements UmbSearchDataSource +{ #host: UmbControllerHost; /** @@ -28,11 +30,12 @@ export class UmbMemberSearchServerDataSource implements UmbSearchDataSource memberReference.unique), }), ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member.search-provider.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member.search-provider.ts index ab714dca8c..01da79605a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member.search-provider.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/member.search-provider.ts @@ -1,16 +1,15 @@ -import type { UmbMemberItemModel } from '../repository/item/types.js'; import { UmbMemberSearchRepository } from './member-search.repository.js'; -import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbMemberSearchItemModel, UmbMemberSearchRequestArgs } from './types.js'; +import type { UmbSearchProvider } from '@umbraco-cms/backoffice/search'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -export interface UmbMemberSearchItemModel extends UmbMemberItemModel { - href: string; -} - -export class UmbMemberSearchProvider extends UmbControllerBase implements UmbSearchProvider { +export class UmbMemberSearchProvider + extends UmbControllerBase + implements UmbSearchProvider +{ #repository = new UmbMemberSearchRepository(this); - async search(args: UmbSearchRequestArgs) { + async search(args: UmbMemberSearchRequestArgs) { return this.#repository.search(args); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/types.ts new file mode 100644 index 0000000000..6ee8fceef5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/types.ts @@ -0,0 +1,11 @@ +import type { UmbMemberItemModel } from '../repository/index.js'; +import type { UmbMemberTypeEntityType } from '@umbraco-cms/backoffice/member-type'; +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; + +export interface UmbMemberSearchItemModel extends UmbMemberItemModel { + href: string; +} + +export interface UmbMemberSearchRequestArgs extends UmbSearchRequestArgs { + allowedContentTypes?: Array<{ unique: string; entityType: UmbMemberTypeEntityType }>; +} 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 9804afba68..69171ad191 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 @@ -200,6 +200,7 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, }, data: { index: index, + isNew: index === null, config: { hideAnchor: this.hideAnchor, }, 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 5efc63199d..7a22b344f0 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,38 +6,67 @@ 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 { umbFocus } from '@umbraco-cms/backoffice/lit-element'; +import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { umbConfirmModal, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbDocumentDetailRepository } from '@umbraco-cms/backoffice/document'; import { UmbMediaDetailRepository } from '@umbraco-cms/backoffice/media'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; -import type { UUIBooleanInputEvent, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIBooleanInputEvent, UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; type UmbInputPickerEvent = CustomEvent & { target: { value?: string } }; @customElement('umb-link-picker-modal') export class UmbLinkPickerModalElement extends UmbModalBaseElement { + #propertyLayoutOrientation: 'horizontal' | 'vertical' = 'vertical'; + + #validationContext = new UmbValidationContext(this); + + @state() + private _allowedMediaTypeUniques?: Array; + @state() private _config: UmbLinkPickerConfig = { hideAnchor: false, hideTarget: false, }; - @state() - private _allowedMediaTypeUniques?: Array; - @query('umb-input-document') private _documentPickerElement?: UmbInputDocumentElement; @query('umb-input-media') private _mediaPickerElement?: UmbInputMediaElement; - override async firstUpdated() { + @query('#link-anchor', true) + private _linkAnchorInput?: UUIInputElement; + + override connectedCallback() { + super.connectedCallback(); + if (this.data?.config) { this._config = this.data.config; } + if (this.modalContext) { + this.observe(this.modalContext.size, (size) => { + if (size === 'large' || size === 'full') { + this.#propertyLayoutOrientation = 'horizontal'; + } + }); + } + + this.#getMediaTypes(); + } + + protected override firstUpdated() { + this._linkAnchorInput?.addValidator( + 'valueMissing', + () => this.localize.term('linkPicker_modalAnchorValidationMessage'), + () => !this.value.link.url && !this.value.link.queryString, + ); + } + + async #getMediaTypes() { // Get all the media types, excluding the folders, so that files are selectable media items. const mediaTypeStructureRepository = new UmbMediaTypeStructureRepository(this); const { data: mediaTypes } = await mediaTypeStructureRepository.requestAllowedChildrenOf(null); @@ -61,7 +90,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement + - ${this.#renderLinkUrlInput()} ${this.#renderLinkTitleInput()} ${this.#renderLinkTargetInput()} - ${this.#renderInternals()} + ${this.#renderLinkType()} ${this.#renderLinkAnchorInput()} ${this.#renderLinkTitleInput()} + ${this.#renderLinkTargetInput()}
+ label=${this.localize.term(this.modalContext?.data.isNew ? 'general_add' : 'general_update')} + ?disabled=${!this.value.link.type} + @click=${this.#onSubmit}>
`; } - #renderLinkUrlInput() { + #renderLinkType() { return html` - -
- - - - - ${when( - !this._config.hideAnchor, - () => html` - - - - `, - )} + +
+ ${this.#renderLinkTypeSelection()} ${this.#renderDocumentPicker()} ${this.#renderMediaPicker()} + ${this.#renderLinkUrlInput()} ${this.#renderLinkUrlInputReadOnly()}
`; } + #renderLinkTypeSelection() { + if (this.value.link.type) return nothing; + return html` + + + + + + `; + } + + #renderDocumentPicker() { + return html` + this.#onPickerSelection(e, 'document')}> + + `; + } + + #renderMediaPicker() { + return html` + this.#onPickerSelection(e, 'media')}> + `; + } + + #renderLinkUrlInput() { + if (this.value.link.type !== 'external') return nothing; + return html` + + ${when( + !this.value.link.unique, + () => html` +
+ + + +
+ `, + )} +
+ `; + } + + #renderLinkUrlInputReadOnly() { + if (!this.value.link.unique || !this.value.link.url) return nothing; + return html``; + } + + #renderLinkAnchorInput() { + if (this._config.hideAnchor) return nothing; + return html` + + + + `; + } + #renderLinkTitleInput() { return html` - + + -
- ${when( - !this.value.link.unique, - () => html` - - - - - `, - )} - this.#onPickerSelection(e, 'document')}> - - this.#onPickerSelection(e, 'media')}> -
-
- `; - } - static override styles = [ css` uui-box { @@ -277,15 +379,9 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement 0 ? num : fallback; + return !Number.isNaN(num) && num > 0 ? num : fallback; } @state() @@ -55,6 +55,9 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement impl @state() private _max = Infinity; + @state() + private _label?: string; + @state() private _alias?: string; @@ -65,11 +68,21 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement impl super(); this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { + this._label = context.getLabel(); this.observe(context.alias, (alias) => (this._alias = alias)); this.observe(context.variantId, (variantId) => (this._variantId = variantId?.toString() || 'invariant')); }); } + protected override firstUpdated() { + if (this._min && this._max && this._min > this._max) { + console.warn( + `Property '${this._label}' (Multi URL Picker) has been misconfigured, 'min' is greater than 'max'. Please correct your data type configuration.`, + this, + ); + } + } + #onChange(event: CustomEvent & { target: UmbInputMultiUrlElement }) { this.value = event.target.urls; this.dispatchEvent(new UmbPropertyValueChangeEvent()); diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/tiny-mce-plugin/tiny-mce-multi-url-picker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/tiny-mce-plugin/tiny-mce-multi-url-picker.plugin.ts index 6bdc8e54ff..436df2067c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/tiny-mce-plugin/tiny-mce-multi-url-picker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/tiny-mce-plugin/tiny-mce-multi-url-picker.plugin.ts @@ -81,6 +81,7 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase data: { config: {}, index: null, + isNew: currentTarget?.url === undefined, }, value: { link: currentTarget ?? {}, diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/components/input-checkbox-list/input-checkbox-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/components/input-checkbox-list/input-checkbox-list.element.ts index 14ee1cf435..405039641d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/components/input-checkbox-list/input-checkbox-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/components/input-checkbox-list/input-checkbox-list.element.ts @@ -1,10 +1,10 @@ -import { css, html, nothing, repeat, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, nothing, repeat, customElement, property, classMap } from '@umbraco-cms/backoffice/external/lit'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; -type UmbCheckboxListItem = { label: string; value: string; checked: boolean }; +export type UmbCheckboxListItem = { label: string; value: string; checked: boolean; invalid?: boolean }; @customElement('umb-input-checkbox-list') export class UmbInputCheckboxListElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -74,16 +74,22 @@ export class UmbInputCheckboxListElement extends UUIFormControlMixin(UmbLitEleme #renderCheckbox(item: (typeof this.list)[0]) { return html``; } - static override styles = [ + static override readonly styles = [ css` uui-checkbox { width: 100%; + + &.invalid { + text-decoration: line-through; + } } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts index 5137551757..2b9e8f7790 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts @@ -1,4 +1,7 @@ -import type { UmbInputCheckboxListElement } from './components/input-checkbox-list/input-checkbox-list.element.js'; +import type { + UmbCheckboxListItem, + UmbInputCheckboxListElement, +} from './components/input-checkbox-list/input-checkbox-list.element.js'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; @@ -29,7 +32,7 @@ export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement implem const items = config.getValueByAlias('items'); - if (Array.isArray(items) && items.length > 0) { + if (Array.isArray(items) && items.length) { this._list = typeof items[0] === 'string' ? items.map((item) => ({ label: item, value: item, checked: this.#selection.includes(item) })) @@ -38,6 +41,13 @@ export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement implem value: item.value, checked: this.#selection.includes(item.value), })); + + // If selection includes a value that is not in the list, add it to the list + this.#selection.forEach((value) => { + if (!this._list.find((item) => item.value === value)) { + this._list.push({ label: value, value, checked: true, invalid: true }); + } + }); } } @@ -51,7 +61,7 @@ export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement implem readonly = false; @state() - private _list: UmbInputCheckboxListElement['list'] = []; + private _list: Array = []; #onChange(event: CustomEvent & { target: UmbInputCheckboxListElement }) { this.value = event.target.selection; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts index 7adc00653e..20eafce705 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts @@ -117,6 +117,17 @@ export class UmbPropertyEditorUIContentPickerElement override firstUpdated() { this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-content')!); this.#setPickerRootUnique(); + + if (this._min && this._max && this._min > this._max) { + console.warn( + `Property (Content Picker) has been misconfigured, 'minNumber' is greater than 'maxNumber'. Please correct your data type configuration.`, + this, + ); + } + } + + override focus() { + return this.shadowRoot?.querySelector('umb-input-content')?.focus(); } async #setPickerRootUnique() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts index 66858004ba..3d18338a4f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts @@ -1,4 +1,4 @@ -import { css, customElement, html, map, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, map, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; import { UUISelectElement } from '@umbraco-cms/backoffice/external/uui'; @@ -46,6 +46,18 @@ export class UmbPropertyEditorUIDropdownElement extends UmbLitElement implements value: item.value, selected: this.#selection.includes(item.value), })); + + // If selection includes a value that is not in the list, add it to the list + this.#selection.forEach((value) => { + if (!this._options.find((item) => item.value === value)) { + this._options.push({ + name: `${value} (${this.localize.term('validation_legacyOption')})`, + value, + selected: true, + invalid: true, + }); + } + }); } this._multiple = config.getValueByAlias('multiple') ?? false; @@ -55,7 +67,7 @@ export class UmbPropertyEditorUIDropdownElement extends UmbLitElement implements private _multiple: boolean = false; @state() - private _options: Array`, )} + ${this.#renderDropdownValidation()} `; } @@ -99,15 +112,34 @@ export class UmbPropertyEditorUIDropdownElement extends UmbLitElement implements .options=${this._options} @change=${this.#onChange} ?readonly=${this.readonly}> + ${this.#renderDropdownValidation()} `; } - static override styles = [ + #renderDropdownValidation() { + const selectionHasInvalids = this.#selection.some((value) => { + const option = this._options.find((item) => item.value === value); + return option ? option.invalid : false; + }); + + if (selectionHasInvalids) { + return html`
`; + } + + return nothing; + } + + static override readonly styles = [ UUISelectElement.styles, css` #native { height: auto; } + + .error { + color: var(--uui-color-danger); + font-size: var(--uui-font-size-small); + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts index 3a80825817..8cbeee6b93 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/multiple-text-string/property-editor-ui-multiple-text-string.element.ts @@ -7,6 +7,7 @@ import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; /** * @element umb-property-editor-ui-multiple-text-string @@ -50,12 +51,31 @@ export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement @property({ type: Boolean, reflect: true }) required = false; + @state() + private _label?: string; + @state() private _min = 0; @state() private _max = Infinity; + constructor() { + super(); + this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { + this._label = context.getLabel(); + }); + } + + protected override firstUpdated() { + if (this._min && this._max && this._min > this._max) { + console.warn( + `Property '${this._label}' (Multiple Text String) has been misconfigured, 'min' is greater than 'max'. Please correct your data type configuration.`, + this, + ); + } + } + #onChange(event: UmbChangeEvent) { event.stopPropagation(); const target = event.currentTarget as UmbInputMultipleTextStringElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/number/property-editor-ui-number.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/number/property-editor-ui-number.element.ts index 5f48f61ba1..23663cb0fd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/number/property-editor-ui-number.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/number/property-editor-ui-number.element.ts @@ -1,16 +1,18 @@ import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; @customElement('umb-property-editor-ui-number') -export class UmbPropertyEditorUINumberElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property({ type: Number }) - value?: number; - +export class UmbPropertyEditorUINumberElement + extends UmbFormControlMixin(UmbLitElement) + implements UmbPropertyEditorUiElement +{ /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. * @type {boolean} @@ -20,6 +22,9 @@ export class UmbPropertyEditorUINumberElement extends UmbLitElement implements U @property({ type: Boolean, reflect: true }) readonly = false; + @state() + private _label?: string; + @state() private _max?: number; @@ -34,12 +39,47 @@ export class UmbPropertyEditorUINumberElement extends UmbLitElement implements U public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - this._min = this.#parseInt(config.getValueByAlias('min')); - this._max = this.#parseInt(config.getValueByAlias('max')); + this._min = this.#parseInt(config.getValueByAlias('min')) || 0; + this._max = this.#parseInt(config.getValueByAlias('max')) || Infinity; this._step = this.#parseInt(config.getValueByAlias('step')); this._placeholder = config.getValueByAlias('placeholder'); } + constructor() { + super(); + + this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { + this._label = context.getLabel(); + }); + + this.addValidator( + 'rangeUnderflow', + () => this.localize.term('validation_numberMinimum', this._min), + () => !!this._min && this.value! < this._min, + ); + + this.addValidator( + 'rangeOverflow', + () => this.localize.term('validation_numberMaximum', this._max), + () => !!this._max && this.value! > this._max, + ); + + this.addValidator( + 'customError', + () => this.localize.term('validation_numberMisconfigured', this._min, this._max), + () => !!this._min && !!this._max && this._min > this._max, + ); + } + + protected override firstUpdated() { + if (this._min && this._max && this._min > this._max) { + console.warn( + `Property '${this._label}' (Numeric) has been misconfigured, 'min' is greater than 'max'. Please correct your data type configuration.`, + this, + ); + } + } + #parseInt(input: unknown): number | undefined { const num = Number(input); return Number.isNaN(num) ? undefined : num; @@ -54,11 +94,12 @@ export class UmbPropertyEditorUINumberElement extends UmbLitElement implements U return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/radio-button-list/property-editor-ui-radio-button-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/radio-button-list/property-editor-ui-radio-button-list.element.ts index e88a0da874..875411a640 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/radio-button-list/property-editor-ui-radio-button-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/radio-button-list/property-editor-ui-radio-button-list.element.ts @@ -1,4 +1,4 @@ -import type { UmbInputRadioButtonListElement } from '@umbraco-cms/backoffice/components'; +import type { UmbInputRadioButtonListElement, UmbRadioButtonItem } from '@umbraco-cms/backoffice/components'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; @@ -34,11 +34,16 @@ export class UmbPropertyEditorUIRadioButtonListElement extends UmbLitElement imp typeof items[0] === 'string' ? items.map((item) => ({ label: item, value: item })) : items.map((item) => ({ label: item.name, value: item.value })); + + // If selection includes a value that is not in the list, add it to the list + if (this.value && !this._list.find((item) => item.value === this.value)) { + this._list.push({ label: this.value, value: this.value, invalid: true }); + } } } @state() - private _list: UmbInputRadioButtonListElement['list'] = []; + private _list: Array = []; #onChange(event: CustomEvent & { target: UmbInputRadioButtonListElement }) { this.value = event.target.value; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts index 9342f2898e..d9a719922c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts @@ -1,6 +1,7 @@ import type { UmbInputSliderElement } from '@umbraco-cms/backoffice/components'; import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorConfigCollection, @@ -27,22 +28,25 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U readonly = false; @state() - _enableRange = false; + private _enableRange = false; @state() - _initVal1: number = 0; + private _initVal1: number = 0; @state() - _initVal2: number = 1; + private _initVal2: number = 1; @state() - _step = 1; + private _label?: string; @state() - _min = 0; + private _step = 1; @state() - _max = 100; + private _min = 0; + + @state() + private _max = 100; public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; @@ -59,21 +63,39 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U const initVal2 = Number(config.getValueByAlias('initVal2')); this._initVal2 = isNaN(initVal2) ? this._initVal1 + this._step : initVal2; - const minVal = Number(config.getValueByAlias('minVal')); - this._min = isNaN(minVal) ? 0 : minVal; - - const maxVal = Number(config.getValueByAlias('maxVal')); - this._max = isNaN(maxVal) ? 100 : maxVal; + this._min = this.#parseInt(config.getValueByAlias('minVal')) || 0; + this._max = this.#parseInt(config.getValueByAlias('maxVal')) || 100; if (this._min === this._max) { this._max = this._min + 100; - //TODO Maybe we want to show some kind of error element rather than trying to fix the mistake made by the user...? - throw new Error( - `Property Editor Slider: min and max are currently equal. Please change your data type configuration. To render the slider correctly, we changed this slider to: min = ${this._min}, max = ${this._max}`, + console.warn( + `Property Editor (Slider) has been misconfigured, 'min' and 'max' are equal values. Please correct your data type configuration. To render the slider correctly, we changed this slider to: min = ${this._min}, max = ${this._max}`, + this, ); } } + constructor() { + super(); + this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { + this._label = context.getLabel(); + }); + } + + protected override firstUpdated() { + if (this._min && this._max && this._min > this._max) { + console.warn( + `Property '${this._label}' (Slider) has been misconfigured, 'min' is greater than 'max'. Please correct your data type configuration.`, + this, + ); + } + } + + #parseInt(input: unknown): number | undefined { + const num = Number(input); + return Number.isNaN(num) ? undefined : num; + } + #getValueObject(value: string) { const [from, to] = value.split(',').map(Number); return { from, to: to ?? from }; @@ -87,6 +109,7 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U override render() { return html` (UmbLitElement, undefined) + extends UmbFormControlMixin(UmbLitElement, undefined) implements UmbPropertyEditorUiElement { /** @@ -23,6 +24,9 @@ export class UmbPropertyEditorUITextareaElement @property({ type: Boolean, reflect: true }) readonly = false; + @state() + private _label?: string; + @state() private _maxChars?: number; @@ -50,8 +54,22 @@ export class UmbPropertyEditorUITextareaElement }; } + constructor() { + super(); + this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => { + this._label = context.getLabel(); + }); + } + protected override firstUpdated(): void { this.addFormControlElement(this.shadowRoot!.querySelector('uui-textarea')!); + + if (this._minHeight && this._maxHeight && this._minHeight > this._maxHeight) { + console.warn( + `Property '${this._label}' (Textarea) has been misconfigured, 'minHeight' is greater than 'maxHeight'. Please correct your data type configuration.`, + this, + ); + } } #onInput(event: InputEvent) { @@ -64,7 +82,7 @@ export class UmbPropertyEditorUITextareaElement override render() { return html` ; } -export interface UmbSearchDataSource { - search(args: UmbSearchRequestArgs): Promise>>; +export interface UmbSearchDataSource< + SearchResultItemType extends UmbSearchResultItemModel, + RequestArgsType extends UmbSearchRequestArgs = UmbSearchRequestArgs, +> { + search(args: RequestArgsType): Promise>>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/search-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/search/search-repository.interface.ts index d13705beb1..bcd77f3773 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/search-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/search/search-repository.interface.ts @@ -1,6 +1,9 @@ import type { UmbSearchRequestArgs, UmbSearchResultItemModel } from './types.js'; import type { UmbRepositoryResponse, UmbPagedModel } from '@umbraco-cms/backoffice/repository'; -export interface UmbSearchRepository { - search(args: UmbSearchRequestArgs): Promise>>; +export interface UmbSearchRepository< + SearchResultItemType extends UmbSearchResultItemModel, + SearchRequestArgsType extends UmbSearchRequestArgs = UmbSearchRequestArgs, +> { + search(args: SearchRequestArgsType): Promise>>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/utils/index.ts index 43446d5e20..224c7dc45c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/utils/index.ts @@ -45,7 +45,7 @@ export const getUmbracoFieldSnippet = (field: string, defaultValue: string | nul const value = `${field !== null ? `@Model.Value("${field}"` : ''}${ fallback !== null ? `, fallback: ${fallback}` : '' - }${defaultValue !== null ? `, defaultValue: new HtmlString("${defaultValue}")` : ''}${field ? ')' : ')'}`; + }${defaultValue !== null ? `, defaultValue: (object)"${defaultValue}"` : ''}${field ? ')' : ')'}`; return value; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index e7e4ca1556..7c6e354bdf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -231,19 +231,18 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { readonly #uploadImageHandler: RawEditorOptions['images_upload_handler'] = (blobInfo, progress) => { return new Promise((resolve, reject) => { - // Fetch does not support progress, so we need to fake it. progress(0); const id = UmbId.new(); const fileBlob = blobInfo.blob(); const file = new File([fileBlob], blobInfo.filename(), { type: fileBlob.type }); - progress(50); - document.dispatchEvent(new CustomEvent('rte.file.uploading', { composed: true, bubbles: true })); this.#temporaryFileRepository - .upload(id, file) + .upload(id, file, (evt) => { + progress((evt.loaded / evt.total) * 100); + }) .then((response) => { if (response.error) { reject(response.error); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index 359e84376d..8ad7178036 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -1,39 +1,26 @@ import type { UmbTiptapExtensionApi } from '../../extensions/types.js'; import type { UmbTiptapToolbarValue } from '../types.js'; -import { css, customElement, html, property, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state, unsafeCSS, when } from '@umbraco-cms/backoffice/external/lit'; import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { Editor, Placeholder, StarterKit, TextStyle } from '@umbraco-cms/backoffice/external/tiptap'; +import { Editor } from '@umbraco-cms/backoffice/external/tiptap'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; +import type { Extensions } from '@umbraco-cms/backoffice/external/tiptap'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import './tiptap-hover-menu.element.js'; import './tiptap-toolbar.element.js'; +const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials'; + @customElement('umb-input-tiptap') export class UmbInputTiptapElement extends UmbFormControlMixin(UmbLitElement) { - readonly #requiredExtensions = [ - StarterKit, - Placeholder.configure({ - placeholder: ({ node }) => { - if (node.type.name === 'heading') { - return this.localize.term('placeholders_rteHeading'); - } - - return this.localize.term('placeholders_rteParagraph'); - }, - }), - TextStyle, - ]; - - @state() - private readonly _extensions: Array = []; - @property({ type: String }) override set value(value: string) { - this.#markup = value; + this.#value = value; // Try to set the value to the editor if it is ready. if (this._editor) { @@ -41,10 +28,9 @@ export class UmbInputTiptapElement extends UmbFormControlMixin = []; + + @state() + private _styles: Array = []; + @state() _toolbar: UmbTiptapToolbarValue = [[[]]]; @@ -76,7 +68,13 @@ export class UmbInputTiptapElement extends UmbFormControlMixin((resolve) => { this.observe(umbExtensionsRegistry.byType('tiptapExtension'), async (manifests) => { - const enabledExtensions = this.configuration?.getValueByAlias('extensions') ?? []; + let enabledExtensions = this.configuration?.getValueByAlias('extensions') ?? []; + + // Ensures that the "Rich Text Essentials" extension is always enabled. [LK] + if (!enabledExtensions.includes(TIPTAP_CORE_EXTENSION_ALIAS)) { + enabledExtensions = [TIPTAP_CORE_EXTENSION_ALIAS, ...enabledExtensions]; + } + for (const manifest of manifests) { if (manifest.api) { const extension = await loadManifestApi(manifest.api); @@ -107,20 +105,30 @@ export class UmbInputTiptapElement extends UmbFormControlMixin('toolbar') ?? [[[]]]; - const extensions = this._extensions - .map((ext) => ext.getTiptapExtensions({ configuration: this.configuration })) - .flat(); + const tiptapExtensions: Extensions = []; + + this._extensions.forEach((ext) => { + const tiptapExt = ext.getTiptapExtensions({ configuration: this.configuration }); + if (tiptapExt?.length) { + tiptapExtensions.push(...tiptapExt); + } + + const styles = ext.getStyles(); + if (styles) { + this._styles.push(styles); + } + }); this._editor = new Editor({ element: element, editable: !this.readonly, - extensions: [...this.#requiredExtensions, ...extensions], - content: this.#markup, + extensions: tiptapExtensions, + content: this.#value, onBeforeCreate: ({ editor }) => { this._extensions.forEach((ext) => ext.setEditor(editor)); }, onUpdate: ({ editor }) => { - this.#markup = editor.getHTML(); + this.#value = editor.getHTML(); this.dispatchEvent(new UmbChangeEvent()); }, }); @@ -132,6 +140,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin html`
`, () => html` + ${this.#renderStyles()} + ${this._styles.map((style) => unsafeCSS(style))} + + `; + } + static override readonly styles = [ css` :host { @@ -165,23 +183,6 @@ export class UmbInputTiptapElement extends UmbFormControlMixin p, - img { - pointer-events: none; - margin: 0; - padding: 0; - } - - &.ProseMirror-selectednode { - outline: 3px solid var(--uui-color-focus); - } - } - - img { - &.ProseMirror-selectednode { - outline: 3px solid var(--uui-color-focus); - } - } - li { > p { margin: 0; padding: 0; } } - - .umb-embed-holder { - display: inline-block; - position: relative; - } - - .umb-embed-holder > * { - user-select: none; - pointer-events: none; - } - - .umb-embed-holder.ProseMirror-selectednode { - outline: 2px solid var(--uui-palette-spanish-pink-light); - } - - .umb-embed-holder::before { - z-index: 1000; - width: 100%; - height: 100%; - position: absolute; - content: ' '; - } - - .umb-embed-holder.ProseMirror-selectednode::before { - background: rgba(0, 0, 0, 0.025); - } - - /* Table-specific styling */ - .tableWrapper { - margin: 1.5rem 0; - overflow-x: auto; - - table { - border-collapse: collapse; - margin: 0; - overflow: hidden; - table-layout: fixed; - width: 100%; - - td, - th { - border: 1px solid var(--uui-color-border); - box-sizing: border-box; - min-width: 1em; - padding: 6px 8px; - position: relative; - vertical-align: top; - - > * { - margin-bottom: 0; - } - } - - th { - background-color: var(--uui-color-background); - font-weight: bold; - text-align: left; - } - - .selectedCell:after { - background: var(--uui-color-surface-emphasis); - content: ''; - left: 0; - right: 0; - top: 0; - bottom: 0; - pointer-events: none; - position: absolute; - z-index: 2; - } - - .column-resize-handle { - background-color: var(--uui-color-default); - bottom: -2px; - pointer-events: none; - position: absolute; - right: -2px; - top: 0; - width: 3px; - } - } - - .resize-cursor { - cursor: ew-resize; - cursor: col-resize; - } - } } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/base.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/base.ts index 5911ae8bc0..7a2b218b3e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/base.ts @@ -6,6 +6,7 @@ import type { UmbTiptapToolbarElementApi, } from './types.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; import type { Editor, Extension, Mark, Node } from '@umbraco-cms/backoffice/external/tiptap'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; @@ -27,6 +28,13 @@ export abstract class UmbTiptapExtensionApiBase extends UmbControllerBase implem this._editor = editor; } + /** + * @inheritdoc + */ + getStyles(): CSSResultGroup | null | undefined { + return null; + } + /** * @inheritdoc */ diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/embedded-media.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/embedded-media.tiptap-api.ts index 4cbc0188f2..cffceb3d74 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/embedded-media.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/embedded-media.tiptap-api.ts @@ -1,6 +1,35 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; +import { css } from '@umbraco-cms/backoffice/external/lit'; import { umbEmbeddedMedia } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapEmbeddedMediaExtensionApi extends UmbTiptapExtensionApiBase { getTiptapExtensions = () => [umbEmbeddedMedia.configure({ inline: true })]; + + override getStyles = () => css` + .umb-embed-holder { + display: inline-block; + position: relative; + } + + .umb-embed-holder > * { + user-select: none; + pointer-events: none; + } + + .umb-embed-holder.ProseMirror-selectednode { + outline: 2px solid var(--uui-palette-spanish-pink-light); + } + + .umb-embed-holder::before { + z-index: 1000; + width: 100%; + height: 100%; + position: absolute; + content: ' '; + } + + .umb-embed-holder.ProseMirror-selectednode::before { + background: rgba(0, 0, 0, 0.025); + } + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/image.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/image.tiptap-api.ts index 7bb79bf2c1..1d7a2da449 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/image.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/image.tiptap-api.ts @@ -1,8 +1,28 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; +import { css } from '@umbraco-cms/backoffice/external/lit'; import { UmbImage } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapImageExtensionApi extends UmbTiptapExtensionApiBase { - getTiptapExtensions() { - return [UmbImage.configure({ inline: true })]; - } + getTiptapExtensions = () => [UmbImage.configure({ inline: true })]; + + override getStyles = () => css` + figure { + > p, + img { + pointer-events: none; + margin: 0; + padding: 0; + } + + &.ProseMirror-selectednode { + outline: 3px solid var(--uui-color-focus); + } + } + + img { + &.ProseMirror-selectednode { + outline: 3px solid var(--uui-color-focus); + } + } + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts new file mode 100644 index 0000000000..bee52bf76d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts @@ -0,0 +1,57 @@ +import { UmbTiptapExtensionApiBase } from '../base.js'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { + Div, + HtmlGlobalAttributes, + Placeholder, + Span, + StarterKit, + TextStyle, +} from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase { + #localize = new UmbLocalizationController(this); + + getTiptapExtensions = () => [ + StarterKit, + Placeholder.configure({ + placeholder: ({ node }) => { + return this.#localize.term( + node.type.name === 'heading' ? 'placeholders_rteHeading' : 'placeholders_rteParagraph', + ); + }, + }), + TextStyle, + HtmlGlobalAttributes.configure({ + types: [ + 'bold', + 'blockquote', + 'bulletList', + 'codeBlock', + 'div', + 'figcaption', + 'figure', + 'heading', + 'horizontalRule', + 'italic', + 'image', + 'link', + 'orderedList', + 'paragraph', + 'span', + 'strike', + 'subscript', + 'superscript', + 'table', + 'tableHeader', + 'tableRow', + 'tableCell', + 'textStyle', + 'underline', + 'umbLink', + ], + }), + Div, + Span, + ]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/table.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/table.tiptap-api.ts index d3acf8c647..6f2ec47cf5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/table.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/table.tiptap-api.ts @@ -1,6 +1,69 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; +import { css } from '@umbraco-cms/backoffice/external/lit'; import { Table, TableHeader, TableRow, TableCell } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapTableExtensionApi extends UmbTiptapExtensionApiBase { getTiptapExtensions = () => [Table.configure({ resizable: true }), TableHeader, TableRow, TableCell]; + + override getStyles = () => css` + .tableWrapper { + margin: 1.5rem 0; + overflow-x: auto; + + table { + border-collapse: collapse; + margin: 0; + overflow: hidden; + table-layout: fixed; + width: 100%; + + td, + th { + border: 1px solid var(--uui-color-border); + box-sizing: border-box; + min-width: 1em; + padding: 6px 8px; + position: relative; + vertical-align: top; + + > * { + margin-bottom: 0; + } + } + + th { + background-color: var(--uui-color-background); + font-weight: bold; + text-align: left; + } + + .selectedCell:after { + background: var(--uui-color-surface-emphasis); + content: ''; + left: 0; + right: 0; + top: 0; + bottom: 0; + pointer-events: none; + position: absolute; + z-index: 2; + } + + .column-resize-handle { + background-color: var(--uui-color-default); + bottom: -2px; + pointer-events: none; + position: absolute; + right: -2px; + top: 0; + width: 3px; + } + } + + .resize-cursor { + cursor: ew-resize; + cursor: col-resize; + } + } + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 07bd7e0b16..9e2e9fd7ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -15,6 +15,19 @@ const kinds: Array = [ ]; const coreExtensions: Array = [ + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.RichTextEssentials', + name: 'Rich Text Essentials Tiptap Extension', + api: () => import('./core/rich-text-essentials.tiptap-api.js'), + weight: 1000, + meta: { + icon: 'icon-browser-window', + label: 'Rich Text Essentials', + group: '#tiptap_extGroup_formatting', + description: 'This is a core extension, it is always enabled by default.', + }, + }, { type: 'tiptapExtension', alias: 'Umb.Tiptap.Embed', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap.extension.ts index 351325ec37..3c5ea41df4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap.extension.ts @@ -11,6 +11,7 @@ export interface MetaTiptapExtension { icon: string; label: string; group: string; + description?: string; } declare global { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/link.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/link.tiptap-toolbar-api.ts index 0af54ff9bc..0761ee50af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/link.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/link.tiptap-toolbar-api.ts @@ -10,7 +10,7 @@ export default class UmbTiptapToolbarLinkExtensionApi extends UmbTiptapToolbarEl override async execute(editor?: Editor) { const attrs = editor?.getAttributes(UmbLink.name) ?? {}; const link = this.#getLinkData(attrs); - const data = { config: {}, index: null }; + const data = { config: {}, index: null, isNew: link?.url === undefined }; const value = { link }; const overlaySize = this.configuration?.getValueByAlias('overlaySize') ?? 'small'; 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 e77af1787d..ddbd215c82 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 @@ -53,6 +53,11 @@ export default class UmbTiptapToolbarMediaPickerToolbarExtensionApi extends UmbT if (!selection?.length) return; const mediaGuid = selection[0]; + + if (!mediaGuid) { + throw new Error('No media selected'); + } + const media = await this.#showMediaCaptionAltText(mediaGuid, currentAltText, currentCaption); if (!media) return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/source-editor.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/source-editor.tiptap-toolbar-api.ts index b40bd86234..459f4ea4b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/source-editor.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/source-editor.tiptap-toolbar-api.ts @@ -13,6 +13,7 @@ export default class UmbTiptapToolbarSourceEditorExtensionApi extends UmbTiptapT headline: 'Edit source code', content: editor?.getHTML() ?? '', language: 'html', + formatOnLoad: true, }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts index 0791bf6b2a..b172f1dae1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts @@ -1,5 +1,6 @@ import type { ManifestTiptapExtension } from './tiptap.extension.js'; import type { ManifestTiptapToolbarExtension } from './tiptap-toolbar.extension.js'; +import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; import type { Editor, Extension, Mark, Node } from '@umbraco-cms/backoffice/external/tiptap'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; @@ -18,6 +19,11 @@ export interface UmbTiptapExtensionApi extends UmbApi { */ setEditor(editor: Editor): void; + /** + * Gets the styles for the extension + */ + getStyles(): CSSResultGroup | null | undefined; + /** * Gets the Tiptap extensions for the editor. */ diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts index 40fabc53e4..5f9756f9af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts @@ -39,9 +39,7 @@ type UmbTiptapExtensionGroup = { const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials'; const TIPTAP_BLOCK_EXTENSION_ALIAS = 'Umb.Tiptap.Block'; -const elementName = 'umb-property-editor-ui-tiptap-extensions-configuration'; - -@customElement(elementName) +@customElement('umb-property-editor-ui-tiptap-extensions-configuration') export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement extends UmbLitElement implements UmbPropertyEditorUiElement @@ -101,16 +99,13 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement this.observe(umbExtensionsRegistry.byType('tiptapExtension'), (extensions) => { this._extensions = extensions .sort((a, b) => a.alias.localeCompare(b.alias)) - .map((ext) => ({ alias: ext.alias, label: ext.meta.label, icon: ext.meta.icon, group: ext.meta.group })); - - // Hardcoded core extension - this._extensions.unshift({ - alias: TIPTAP_CORE_EXTENSION_ALIAS, - label: 'Rich Text Essentials', - icon: 'icon-browser-window', - group: '#tiptap_extGroup_formatting', - description: 'This is a core extension, it is always enabled by default.', - }); + .map((ext) => ({ + alias: ext.alias, + label: ext.meta.label, + icon: ext.meta.icon, + group: ext.meta.group, + description: ext.meta.description, + })); if (!this.value) { // The default value is all extensions enabled @@ -226,6 +221,6 @@ export { UmbPropertyEditorUiTiptapExtensionsConfigurationElement as element }; declare global { interface HTMLElementTagNameMap { - [elementName]: UmbPropertyEditorUiTiptapExtensionsConfigurationElement; + 'umb-property-editor-ui-tiptap-extensions-configuration': UmbPropertyEditorUiTiptapExtensionsConfigurationElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts index 2613164ea7..cff1839022 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts @@ -4,12 +4,10 @@ import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; import '../../components/input-tiptap/input-tiptap.element.js'; -const elementName = 'umb-property-editor-ui-tiptap'; - /** * @element umb-property-editor-ui-tiptap */ -@customElement(elementName) +@customElement('umb-property-editor-ui-tiptap') export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElementBase { #onChange(event: CustomEvent & { target: UmbInputTiptapElement }) { const tipTapElement = event.target; @@ -75,6 +73,6 @@ export { UmbPropertyEditorUiTiptapElement as element }; declare global { interface HTMLElementTagNameMap { - [elementName]: UmbPropertyEditorUiTiptapElement; + 'umb-property-editor-ui-tiptap': UmbPropertyEditorUiTiptapElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/change-password/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/change-password/entity-action/manifests.ts index dce4333405..e46f5fc254 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/change-password/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/change-password/entity-action/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_USER_ENTITY_TYPE } from '@umbraco-cms/backoffice/user'; +import { UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS, UMB_USER_ENTITY_TYPE } from '@umbraco-cms/backoffice/user'; export const manifests: Array = [ { @@ -18,7 +18,7 @@ export const manifests: Array = [ alias: 'Umb.Condition.User.IsDefaultKind', }, { - alias: 'Umb.Condition.User.AllowChangePassword', + alias: UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS, }, ], }, 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 4aaf6e2101..4f75473328 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 @@ -1,3 +1,5 @@ +import { UMB_CURRENT_USER_ALLOW_MFA_CONDITION_ALIAS } from '@umbraco-cms/backoffice/user'; + export const manifests: Array = [ { type: 'currentUserAction', @@ -13,7 +15,7 @@ export const manifests: Array = [ }, conditions: [ { - alias: 'Umb.Condition.User.AllowMfaAction', + alias: UMB_CURRENT_USER_ALLOW_MFA_CONDITION_ALIAS, }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/profile/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/profile/manifests.ts index 7f260c2372..7b87322454 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/profile/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/profile/manifests.ts @@ -1,4 +1,5 @@ import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; +import { UMB_CURRENT_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS } from '@umbraco-cms/backoffice/user'; export const manifests: Array = [ { @@ -43,7 +44,7 @@ export const manifests: Array = [ }, conditions: [ { - alias: 'Umb.Condition.User.AllowChangePassword', + alias: UMB_CURRENT_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS, }, ], }, 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 fd9ea76a2b..d59c91de01 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 @@ -50,6 +50,7 @@ export class UmbUserGroupCollectionTableViewElement extends UmbLitElement { { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts index 1adc2fdd07..dc356f6e31 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts @@ -1,11 +1,12 @@ import { UmbUserGroupCollectionRepository } from '../../collection/repository/index.js'; import type { UmbUserGroupDetailModel } from '../../types.js'; -import { customElement, html, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { debounce, UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; -import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import type { UMB_USER_GROUP_PICKER_MODAL } from '@umbraco-cms/backoffice/user-group'; -import type { UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIInputEvent, UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui'; import '../../components/user-group-ref/user-group-ref.element.js'; @@ -14,10 +15,23 @@ export class UmbUserGroupPickerModalElement extends UmbModalBaseElement< typeof UMB_USER_GROUP_PICKER_MODAL.DATA, typeof UMB_USER_GROUP_PICKER_MODAL.VALUE > { + @state() + private _filteredItems: Array = []; + @state() private _userGroups: Array = []; + #debouncedFilter = debounce((filter: string) => { + this._filteredItems = filter + ? this._userGroups.filter( + (userGroup) => + userGroup.alias.toLowerCase().includes(filter) || userGroup.name.toLowerCase().includes(filter), + ) + : this._userGroups; + }, 500); + #selectionManager = new UmbSelectionManager(this); + #userGroupCollectionRepository = new UmbUserGroupCollectionRepository(this); constructor() { @@ -40,7 +54,7 @@ export class UmbUserGroupPickerModalElement extends UmbModalBaseElement< const { error, asObservable } = await this.#userGroupCollectionRepository.requestCollection(); if (error) return; - this.observe(asObservable(), (items) => (this._userGroups = items), 'umbUserGroupsObserver'); + this.observe(asObservable(), (items) => (this._userGroups = this._filteredItems = items), 'umbUserGroupsObserver'); } #onSelected(event: UUIMenuItemEvent, item: UmbUserGroupDetailModel) { @@ -63,6 +77,11 @@ export class UmbUserGroupPickerModalElement extends UmbModalBaseElement< this.modalContext?.dispatchEvent(new UmbDeselectedEvent(item.unique)); } + #onFilterInput(event: UUIInputEvent) { + const query = (event.target.value as string) || ''; + this.#debouncedFilter(query.toLowerCase()); + } + #onSubmit() { this.updateValue({ selection: this.#selectionManager.getSelection() }); this._submitModal(); @@ -70,10 +89,19 @@ export class UmbUserGroupPickerModalElement extends UmbModalBaseElement< override render() { return html` - + + + + ${repeat( - this._userGroups, + this._filteredItems, (userGroup) => userGroup.alias, (userGroup) => html` @@ -104,6 +132,22 @@ export class UmbUserGroupPickerModalElement extends UmbModalBaseElement< `; } + + static override styles = [ + css` + #filter { + width: 100%; + margin-bottom: var(--uui-size-space-4); + } + + #filter-icon { + display: flex; + color: var(--uui-color-border); + height: 100%; + padding-left: var(--uui-size-space-2); + } + `, + ]; } export default UmbUserGroupPickerModalElement; 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 8893980225..3fda2de30f 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 @@ -51,6 +51,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/constants.ts index 31baf82e9f..3b08cd92ce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/constants.ts @@ -1 +1,2 @@ export const UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS = 'Umb.Condition.User.AllowChangePassword'; +export const UMB_CURRENT_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS = 'Umb.Condition.CurrentUser.AllowChangePassword'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/current-user-allow-change-password-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/current-user-allow-change-password-action.condition.ts new file mode 100644 index 0000000000..f510c755b9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/current-user-allow-change-password-action.condition.ts @@ -0,0 +1,27 @@ +import UmbCurrentUserConfigRepository from '../../repository/config/current-user-config.repository.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; +import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; + +export class UmbCurrentUserAllowChangePasswordActionCondition extends UmbConditionBase { + #configRepository = new UmbCurrentUserConfigRepository(this._host); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(host: UmbControllerHost, args: any) { + super(host, args); + this.#init(); + } + + async #init() { + await this.#configRepository.initialized; + this.observe( + this.#configRepository.part('allowChangePassword'), + (isAllowed) => { + this.permitted = isAllowed; + }, + '_userAllowChangePasswordActionCondition', + ); + } +} + +export { UmbCurrentUserAllowChangePasswordActionCondition as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/manifests.ts index 568650836e..3757a4efab 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS } from './constants.js'; +import { UMB_CURRENT_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS, UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS } from './constants.js'; export const manifests: Array = [ { @@ -7,4 +7,10 @@ export const manifests: Array = [ alias: UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS, api: () => import('./user-allow-change-password-action.condition.js'), }, + { + type: 'condition', + name: 'Current User Allow Change Password Condition', + alias: UMB_CURRENT_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS, + api: () => import('./current-user-allow-change-password-action.condition.js'), + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/user-allow-change-password-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/user-allow-change-password-action.condition.ts index 6d2176da44..9200316140 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/user-allow-change-password-action.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-change-password/user-allow-change-password-action.condition.ts @@ -14,7 +14,6 @@ export class UmbUserAllowChangePasswordActionCondition extends UmbConditionBase< async #init() { await this.#configRepository.initialized; - this.observe( this.#configRepository.part('allowChangePassword'), (isAllowed) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/constants.ts index 872ba596a7..592c61daba 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/constants.ts @@ -1 +1,2 @@ export const UMB_USER_ALLOW_MFA_CONDITION_ALIAS = 'Umb.Condition.User.AllowMfaAction'; +export const UMB_CURRENT_USER_ALLOW_MFA_CONDITION_ALIAS = 'Umb.Condition.CurrentUser.AllowMfaAction'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/current-user-allow-mfa-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/current-user-allow-mfa-action.condition.ts new file mode 100644 index 0000000000..4ba1904aa6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/current-user-allow-mfa-action.condition.ts @@ -0,0 +1,30 @@ +import { UmbCurrentUserConfigRepository } from '../../repository/config/index.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbConditionBase, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbCurrentUserAllowMfaActionCondition extends UmbConditionBase { + #configRepository = new UmbCurrentUserConfigRepository(this._host); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(host: UmbControllerHost, args: any) { + super(host, args); + this.#init(); + } + + async #init() { + await this.#configRepository.initialized; + this.observe( + observeMultiple([ + this.#configRepository.part('allowTwoFactor'), + umbExtensionsRegistry.byType('mfaLoginProvider'), + ]), + ([allowTwoFactor, exts]) => { + this.permitted = allowTwoFactor && exts.length > 0; + }, + '_userAllowMfaActionCondition', + ); + } +} + +export { UmbCurrentUserAllowMfaActionCondition as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/manifests.ts index c25c206618..08784712c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_USER_ALLOW_MFA_CONDITION_ALIAS } from './constants.js'; +import { UMB_USER_ALLOW_MFA_CONDITION_ALIAS, UMB_CURRENT_USER_ALLOW_MFA_CONDITION_ALIAS } from './constants.js'; export const manifests: Array = [ { @@ -7,4 +7,10 @@ export const manifests: Array = [ alias: UMB_USER_ALLOW_MFA_CONDITION_ALIAS, api: () => import('./user-allow-mfa-action.condition.js'), }, + { + type: 'condition', + name: 'Current User Allow Mfa Action Condition', + alias: UMB_CURRENT_USER_ALLOW_MFA_CONDITION_ALIAS, + api: () => import('./current-user-allow-mfa-action.condition.js'), + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/user-allow-mfa-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/user-allow-mfa-action.condition.ts index 8534f164a1..08798ba9d6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/user-allow-mfa-action.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/allow-mfa/user-allow-mfa-action.condition.ts @@ -14,7 +14,6 @@ export class UmbUserAllowMfaActionCondition extends UmbConditionBase { async #init() { await this.#configRepository.initialized; - this.observe( observeMultiple([ this.#configRepository.part('allowTwoFactor'), diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts index 3643a46f60..aef9859fd5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_USER_DETAIL_REPOSITORY_ALIAS, UMB_USER_ITEM_REPOSITORY_ALIAS } from '../constants.js'; +import { UMB_USER_ALLOW_MFA_CONDITION_ALIAS, UMB_USER_DETAIL_REPOSITORY_ALIAS, UMB_USER_ITEM_REPOSITORY_ALIAS } from '../constants.js'; import { UMB_USER_ENTITY_TYPE } from '../entity.js'; import { manifests as createManifests } from './create/manifests.js'; @@ -92,7 +92,7 @@ const entityActions: Array = [ }, conditions: [ { - alias: 'Umb.Condition.User.AllowMfaAction', + alias: UMB_USER_ALLOW_MFA_CONDITION_ALIAS, }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts index 76356d5956..e79f930e88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts @@ -48,7 +48,7 @@ export class UmbUserPickerModalElement extends UmbModalBaseElement + ${this._users.map( (user) => html` @@ -68,8 +68,12 @@ export class UmbUserPickerModalElement extends UmbModalBaseElement
- - + +
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/constants.ts index 074c0c2cf3..7582001709 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/constants.ts @@ -1,3 +1,7 @@ export const UMB_USER_CONFIG_REPOSITORY_ALIAS = 'Umb.Repository.User.Config'; export const UMB_USER_CONFIG_STORE_ALIAS = 'Umb.Store.User.Config'; export { UMB_USER_CONFIG_STORE_CONTEXT } from './user-config.store.token.js'; + +export const UMB_CURRENT_USER_CONFIG_REPOSITORY_ALIAS = 'Umb.Repository.CurrentUser.Config'; +export const UMB_CURRENT_USER_CONFIG_STORE_ALIAS = 'Umb.Store.CurrentUser.Config'; +export { UMB_CURRENT_USER_CONFIG_STORE_CONTEXT } from './current-user-config.store.token.js'; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.repository.ts new file mode 100644 index 0000000000..01311a0f33 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.repository.ts @@ -0,0 +1,67 @@ +import type { UmbCurrentUserConfigurationModel } from '../../types.js'; +import { UmbCurrentUserConfigServerDataSource } from './current-user-config.server.data-source.js'; +import { UMB_CURRENT_USER_CONFIG_STORE_CONTEXT } from './current-user-config.store.token.js'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { Observable } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbCurrentUserConfigRepository extends UmbRepositoryBase implements UmbApi { + /** + * Promise that resolves when the repository has been initialized, i.e. when the user configuration has been fetched from the server. + * @memberof UmbCurrentUserConfigRepository + */ + initialized: Promise; + + #dataStore?: typeof UMB_CURRENT_USER_CONFIG_STORE_CONTEXT.TYPE; + #dataSource = new UmbCurrentUserConfigServerDataSource(this); + + constructor(host: UmbControllerHost) { + super(host); + this.initialized = new Promise((resolve) => { + this.consumeContext(UMB_CURRENT_USER_CONFIG_STORE_CONTEXT, async (store) => { + this.#dataStore = store; + await this.#init(); + resolve(); + }); + }); + } + + async #init() { + // Check if the store already has data + if (this.#dataStore?.getState()) { + return; + } + + const { data } = await this.#dataSource.getCurrentUserConfig(); + + if (data) { + this.#dataStore?.update(data); + } + } + + /** + * Subscribe to the entire user configuration. + */ + all() { + if (!this.#dataStore) { + throw new Error('Data store not initialized'); + } + + return this.#dataStore.all(); + } + + /** + * Subscribe to a part of the user configuration. + * @param part + */ + part(part: Part): Observable { + if (!this.#dataStore) { + throw new Error('Data store not initialized'); + } + + return this.#dataStore.part(part); + } +} + +export default UmbCurrentUserConfigRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.server.data-source.ts new file mode 100644 index 0000000000..5054a2b4b9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.server.data-source.ts @@ -0,0 +1,19 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UserService } from '@umbraco-cms/backoffice/external/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +export class UmbCurrentUserConfigServerDataSource { + #host; + + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Get the current user configuration. + * @memberof UmbCurrentUserConfigServerDataSource + */ + getCurrentUserConfig() { + return tryExecuteAndNotify(this.#host, UserService.getUserCurrentConfiguration()); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.store.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.store.token.ts new file mode 100644 index 0000000000..63971f9916 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.store.token.ts @@ -0,0 +1,4 @@ +import type { UmbCurrentUserConfigStore } from './current-user-config.store.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_CURRENT_USER_CONFIG_STORE_CONTEXT = new UmbContextToken('UmbCurrentUserConfigStore'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.store.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.store.ts new file mode 100644 index 0000000000..d8c7df249b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/current-user-config.store.ts @@ -0,0 +1,12 @@ +import type { UmbCurrentUserConfigurationModel } from '../../types.js'; +import { UMB_CURRENT_USER_CONFIG_STORE_CONTEXT } from './current-user-config.store.token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbStoreObjectBase } from '@umbraco-cms/backoffice/store'; + +export class UmbCurrentUserConfigStore extends UmbStoreObjectBase { + constructor(host: UmbControllerHost) { + super(host, UMB_CURRENT_USER_CONFIG_STORE_CONTEXT.toString()); + } +} + +export default UmbCurrentUserConfigStore; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/index.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/index.ts index c848c34591..6555849928 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/index.ts @@ -1,2 +1,3 @@ export * from './constants.js'; +export * from './current-user-config.repository.js'; export * from './user-config.repository.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/manifests.ts index 9fd9b72397..2e0e60ce1e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_USER_CONFIG_REPOSITORY_ALIAS, UMB_USER_CONFIG_STORE_ALIAS } from './constants.js'; +import { UMB_CURRENT_USER_CONFIG_REPOSITORY_ALIAS, UMB_CURRENT_USER_CONFIG_STORE_ALIAS, UMB_USER_CONFIG_REPOSITORY_ALIAS, UMB_USER_CONFIG_STORE_ALIAS } from './constants.js'; export const manifests: Array = [ { @@ -13,4 +13,16 @@ export const manifests: Array = [ name: 'User Config Repository', api: () => import('./user-config.repository.js'), }, + { + type: 'store', + alias: UMB_CURRENT_USER_CONFIG_STORE_ALIAS, + name: 'Current User Config Store', + api: () => import('./current-user-config.store.js'), + }, + { + type: 'repository', + alias: UMB_CURRENT_USER_CONFIG_REPOSITORY_ALIAS, + name: 'Current User Config Repository', + api: () => import('./current-user-config.repository.js'), + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/user-config.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/user-config.server.data-source.ts index a85a0f3311..ced6f1cc52 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/user-config.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/config/user-config.server.data-source.ts @@ -16,4 +16,12 @@ export class UmbUserConfigServerDataSource { getUserConfig() { return tryExecuteAndNotify(this.#host, UserService.getUserConfiguration()); } + + /** + * Get the current user configuration. + * @memberof UmbUserConfigServerDataSource + */ + getCurrentUserConfig() { + return tryExecuteAndNotify(this.#host, UserService.getUserCurrentConfiguration()); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/types.ts index 804493858d..736e5d2983 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/types.ts @@ -2,6 +2,7 @@ import type { UmbUserEntityType } from './entity.js'; import type { UmbUserKindType } from './utils/index.js'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; import { + type CurrenUserConfigurationResponseModel, type UserConfigurationResponseModel, UserStateModel, type UserTwoFactorProviderModel, @@ -42,3 +43,5 @@ export interface UmbUserStartNodesModel { export type UmbUserMfaProviderModel = UserTwoFactorProviderModel; export type UmbUserConfigurationModel = UserConfigurationResponseModel; + +export type UmbCurrentUserConfigurationModel = CurrenUserConfigurationResponseModel; diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/collection/views/table/webhook-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/collection/views/table/webhook-table-collection-view.element.ts index 26e6566274..b731b164cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/collection/views/table/webhook-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/collection/views/table/webhook-table-collection-view.element.ts @@ -42,6 +42,7 @@ export class UmbWebhookTableCollectionViewElement extends UmbLitElement { { name: '', alias: 'entityActions', + align: 'right', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/rollup.config.js b/src/Umbraco.Web.UI.Client/src/rollup.config.js index 8b42033978..daac5a4e65 100644 --- a/src/Umbraco.Web.UI.Client/src/rollup.config.js +++ b/src/Umbraco.Web.UI.Client/src/rollup.config.js @@ -54,6 +54,7 @@ console.log('--- Copying TinyMCE i18n done ---'); console.log('--- Copying monaco-editor ---'); cpSync('./node_modules/monaco-editor/esm/vs/editor/editor.worker.js', `${DIST_DIRECTORY}/monaco-editor/vs/editor/editor.worker.js`); cpSync('./node_modules/monaco-editor/esm/vs/language', `${DIST_DIRECTORY}/monaco-editor/vs/language`, { recursive: true }); +cpSync('./node_modules/monaco-editor/min/vs/base/browser/ui/codicons', `${DIST_DIRECTORY}/assets/fonts`, { recursive: true }); console.log('--- Copying monaco-editor done ---'); const readFolders = (path) => readdirSync(path).filter((folder) => lstatSync(`${path}/${folder}`).isDirectory()); diff --git a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts index f9ee9112d0..81387d9cc9 100644 --- a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts +++ b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/index.ts @@ -52,7 +52,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/auth', - consts: ["UMB_AUTH_CONTEXT","UMB_STORAGE_TOKEN_RESPONSE_NAME","UMB_STORAGE_REDIRECT_URL","UMB_MODAL_APP_AUTH"] + consts: ["UMB_AUTH_CONTEXT","UMB_STORAGE_TOKEN_RESPONSE_NAME","UMB_MODAL_APP_AUTH"] }, { path: '@umbraco-cms/backoffice/block-custom-view', @@ -140,7 +140,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/document', - consts: ["UMB_DOCUMENT_COLLECTION_ALIAS","UMB_DOCUMENT_COLLECTION_CONTEXT","UMB_DOCUMENT_COLLECTION_REPOSITORY_ALIAS","UMB_DOCUMENT_GRID_COLLECTION_VIEW_ALIAS","UMB_DOCUMENT_TABLE_COLLECTION_VIEW_ALIAS","UMB_DOCUMENT_CREATE_OPTIONS_MODAL","UMB_CREATE_BLUEPRINT_MODAL","UMB_DOCUMENT_CREATE_BLUEPRINT_REPOSITORY_ALIAS","UMB_CULTURE_AND_HOSTNAMES_MODAL","UMB_DOCUMENT_CULTURE_AND_HOSTNAMES_REPOSITORY_ALIAS","UMB_DUPLICATE_DOCUMENT_MODAL","UMB_DUPLICATE_DOCUMENT_MODAL_ALIAS","UMB_DUPLICATE_DOCUMENT_REPOSITORY_ALIAS","UMB_MOVE_DOCUMENT_REPOSITORY_ALIAS","UMB_DOCUMENT_NOTIFICATIONS_MODAL","UMB_DOCUMENT_NOTIFICATIONS_MODAL_ALIAS","UMB_DOCUMENT_NOTIFICATIONS_REPOSITORY_ALIAS","UMB_PUBLIC_ACCESS_MODAL","UMB_DOCUMENT_PUBLIC_ACCESS_REPOSITORY_ALIAS","UMB_SORT_CHILDREN_OF_DOCUMENT_REPOSITORY_ALIAS","UMB_BULK_DUPLICATE_DOCUMENT_REPOSITORY_ALIAS","UMB_BULK_MOVE_DOCUMENT_REPOSITORY_ALIAS","UMB_BULK_TRASH_DOCUMENT_REPOSITORY_ALIAS","UMB_DOCUMENT_ENTITY_TYPE","UMB_DOCUMENT_ROOT_ENTITY_TYPE","UMB_DOCUMENT_CONFIGURATION_CONTEXT","UMB_CONTENT_MENU_ALIAS","UMB_DOCUMENT_PICKER_MODAL","UMB_DOCUMENT_SAVE_MODAL","UMB_DOCUMENT_SAVE_MODAL_ALIAS","UMB_DOCUMENT_WORKSPACE_PATH","UMB_CREATE_FROM_BLUEPRINT_DOCUMENT_WORKSPACE_PATH_PATTERN","UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN","UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN","UMB_DOCUMENT_PROPERTY_DATASET_CONTEXT","UMB_DOCUMENT_PUBLISH_MODAL_ALIAS","UMB_DOCUMENT_PUBLISH_MODAL","UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL_ALIAS","UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL","UMB_DOCUMENT_PUBLISHING_REPOSITORY_ALIAS","UMB_DOCUMENT_SCHEDULE_MODAL_ALIAS","UMB_DOCUMENT_SCHEDULE_MODAL","UMB_DOCUMENT_UNPUBLISH_MODAL_ALIAS","UMB_DOCUMENT_UNPUBLISH_MODAL","UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT","UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE","UMB_DOCUMENT_RECYCLE_BIN_REPOSITORY_ALIAS","UMB_DOCUMENT_RECYCLE_BIN_TREE_REPOSITORY_ALIAS","UMB_DOCUMENT_RECYCLE_BIN_TREE_STORE_ALIAS","UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS","UMB_DOCUMENT_RECYCLE_BIN_TREE_STORE_CONTEXT","UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS","UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS","UMB_DOCUMENT_DETAIL_STORE_ALIAS","UMB_DOCUMENT_DETAIL_STORE_CONTEXT","UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS","UMB_DOCUMENT_STORE_ALIAS","UMB_DOCUMENT_ITEM_STORE_CONTEXT","UMB_DOCUMENT_URL_REPOSITORY_ALIAS","UMB_DOCUMENT_URL_STORE_ALIAS","UMB_DOCUMENT_URL_STORE_CONTEXT","UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS","UMB_ROLLBACK_MODAL_ALIAS","UMB_ROLLBACK_MODAL","UMB_ROLLBACK_REPOSITORY_ALIAS","UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS","UMB_DOCUMENT_TREE_STORE_CONTEXT","UMB_DOCUMENT_TREE_REPOSITORY_ALIAS","UMB_DOCUMENT_TREE_STORE_ALIAS","UMB_DOCUMENT_TREE_ALIAS","UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS","UMB_USER_PERMISSION_DOCUMENT_CREATE","UMB_USER_PERMISSION_DOCUMENT_READ","UMB_USER_PERMISSION_DOCUMENT_UPDATE","UMB_USER_PERMISSION_DOCUMENT_DELETE","UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT","UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS","UMB_USER_PERMISSION_DOCUMENT_PUBLISH","UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS","UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH","UMB_USER_PERMISSION_DOCUMENT_DUPLICATE","UMB_USER_PERMISSION_DOCUMENT_MOVE","UMB_USER_PERMISSION_DOCUMENT_SORT","UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES","UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS","UMB_USER_PERMISSION_DOCUMENT_ROLLBACK","UMB_DOCUMENT_PERMISSION_REPOSITORY_ALIAS","UMB_DOCUMENT_IS_NOT_TRASHED_CONDITION_ALIAS","UMB_DOCUMENT_IS_TRASHED_CONDITION_ALIAS","UMB_DOCUMENT_WORKSPACE_ALIAS","UMB_DOCUMENT_DETAIL_MODEL_VARIANT_SCAFFOLD","UMB_DOCUMENT_WORKSPACE_CONTEXT"] + consts: ["UMB_DOCUMENT_COLLECTION_ALIAS","UMB_DOCUMENT_COLLECTION_CONTEXT","UMB_DOCUMENT_COLLECTION_REPOSITORY_ALIAS","UMB_DOCUMENT_GRID_COLLECTION_VIEW_ALIAS","UMB_DOCUMENT_TABLE_COLLECTION_VIEW_ALIAS","UMB_DOCUMENT_CREATE_OPTIONS_MODAL","UMB_CREATE_BLUEPRINT_MODAL","UMB_DOCUMENT_CREATE_BLUEPRINT_REPOSITORY_ALIAS","UMB_CULTURE_AND_HOSTNAMES_MODAL","UMB_DOCUMENT_CULTURE_AND_HOSTNAMES_REPOSITORY_ALIAS","UMB_DUPLICATE_DOCUMENT_MODAL","UMB_DUPLICATE_DOCUMENT_MODAL_ALIAS","UMB_DUPLICATE_DOCUMENT_REPOSITORY_ALIAS","UMB_MOVE_DOCUMENT_REPOSITORY_ALIAS","UMB_DOCUMENT_NOTIFICATIONS_MODAL","UMB_DOCUMENT_NOTIFICATIONS_MODAL_ALIAS","UMB_DOCUMENT_NOTIFICATIONS_REPOSITORY_ALIAS","UMB_PUBLIC_ACCESS_MODAL","UMB_DOCUMENT_PUBLIC_ACCESS_REPOSITORY_ALIAS","UMB_SORT_CHILDREN_OF_DOCUMENT_REPOSITORY_ALIAS","UMB_BULK_DUPLICATE_DOCUMENT_REPOSITORY_ALIAS","UMB_BULK_MOVE_DOCUMENT_REPOSITORY_ALIAS","UMB_BULK_TRASH_DOCUMENT_REPOSITORY_ALIAS","UMB_DOCUMENT_ENTITY_TYPE","UMB_DOCUMENT_ROOT_ENTITY_TYPE","UMB_DOCUMENT_CONFIGURATION_CONTEXT","UMB_CONTENT_MENU_ALIAS","UMB_DOCUMENT_PICKER_MODAL","UMB_DOCUMENT_SAVE_MODAL","UMB_DOCUMENT_SAVE_MODAL_ALIAS","UMB_DOCUMENT_WORKSPACE_PATH","UMB_CREATE_FROM_BLUEPRINT_DOCUMENT_WORKSPACE_PATH_PATTERN","UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN","UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN","UMB_DOCUMENT_PROPERTY_DATASET_CONTEXT","UMB_DOCUMENT_PUBLISH_MODAL_ALIAS","UMB_DOCUMENT_PUBLISH_MODAL","UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL_ALIAS","UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL","UMB_DOCUMENT_PUBLISHING_REPOSITORY_ALIAS","UMB_DOCUMENT_SCHEDULE_MODAL_ALIAS","UMB_DOCUMENT_SCHEDULE_MODAL","UMB_DOCUMENT_UNPUBLISH_MODAL_ALIAS","UMB_DOCUMENT_UNPUBLISH_MODAL","UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT","UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE","UMB_DOCUMENT_RECYCLE_BIN_REPOSITORY_ALIAS","UMB_DOCUMENT_RECYCLE_BIN_TREE_REPOSITORY_ALIAS","UMB_DOCUMENT_RECYCLE_BIN_TREE_STORE_ALIAS","UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS","UMB_DOCUMENT_RECYCLE_BIN_TREE_STORE_CONTEXT","UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS","UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS","UMB_DOCUMENT_DETAIL_STORE_ALIAS","UMB_DOCUMENT_DETAIL_STORE_CONTEXT","UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS","UMB_DOCUMENT_STORE_ALIAS","UMB_DOCUMENT_ITEM_STORE_CONTEXT","UMB_DOCUMENT_VALIDATION_REPOSITORY_ALIAS","UMB_ROLLBACK_MODAL_ALIAS","UMB_ROLLBACK_MODAL","UMB_ROLLBACK_REPOSITORY_ALIAS","UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS","UMB_DOCUMENT_TREE_STORE_CONTEXT","UMB_DOCUMENT_TREE_REPOSITORY_ALIAS","UMB_DOCUMENT_TREE_STORE_ALIAS","UMB_DOCUMENT_TREE_ALIAS","UMB_DOCUMENT_URL_REPOSITORY_ALIAS","UMB_DOCUMENT_URL_STORE_ALIAS","UMB_DOCUMENT_URL_STORE_CONTEXT","UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS","UMB_USER_PERMISSION_DOCUMENT_CREATE","UMB_USER_PERMISSION_DOCUMENT_READ","UMB_USER_PERMISSION_DOCUMENT_UPDATE","UMB_USER_PERMISSION_DOCUMENT_DELETE","UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT","UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS","UMB_USER_PERMISSION_DOCUMENT_PUBLISH","UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS","UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH","UMB_USER_PERMISSION_DOCUMENT_DUPLICATE","UMB_USER_PERMISSION_DOCUMENT_MOVE","UMB_USER_PERMISSION_DOCUMENT_SORT","UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES","UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS","UMB_USER_PERMISSION_DOCUMENT_ROLLBACK","UMB_DOCUMENT_PERMISSION_REPOSITORY_ALIAS","UMB_DOCUMENT_IS_NOT_TRASHED_CONDITION_ALIAS","UMB_DOCUMENT_IS_TRASHED_CONDITION_ALIAS","UMB_DOCUMENT_WORKSPACE_ALIAS","UMB_DOCUMENT_DETAIL_MODEL_VARIANT_SCAFFOLD","UMB_DOCUMENT_WORKSPACE_CONTEXT"] }, { path: '@umbraco-cms/backoffice/entity-action', @@ -212,7 +212,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/media', - consts: ["UMB_MEDIA_COLLECTION_ALIAS","UMB_MEDIA_COLLECTION_CONTEXT","UMB_MEDIA_COLLECTION_REPOSITORY_ALIAS","UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS","UMB_MEDIA_TABLE_COLLECTION_VIEW_ALIAS","UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL","UMB_MEDIA_CREATE_OPTIONS_MODAL","UMB_MOVE_MEDIA_REPOSITORY_ALIAS","UMB_SORT_CHILDREN_OF_MEDIA_REPOSITORY_ALIAS","UMB_BULK_MOVE_MEDIA_REPOSITORY_ALIAS","UMB_BULK_TRASH_MEDIA_REPOSITORY_ALIAS","UMB_MEDIA_ENTITY_TYPE","UMB_MEDIA_ROOT_ENTITY_TYPE","UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE","UMB_MEDIA_MENU_ALIAS","UMB_IMAGE_CROPPER_EDITOR_MODAL","UMB_MEDIA_CAPTION_ALT_TEXT_MODAL","UMB_MEDIA_PICKER_MODAL","UMB_MEDIA_WORKSPACE_PATH","UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN","UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN","UMB_MEDIA_VARIANT_CONTEXT","UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE","UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_STORE_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_STORE_CONTEXT","UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS","UMB_MEDIA_DETAIL_REPOSITORY_ALIAS","UMB_MEDIA_DETAIL_STORE_ALIAS","UMB_MEDIA_DETAIL_STORE_CONTEXT","UMB_MEDIA_ITEM_REPOSITORY_ALIAS","UMB_MEDIA_STORE_ALIAS","UMB_MEDIA_ITEM_STORE_CONTEXT","UMB_MEDIA_URL_REPOSITORY_ALIAS","UMB_MEDIA_URL_STORE_ALIAS","UMB_MEDIA_URL_STORE_CONTEXT","UMB_MEDIA_VALIDATION_REPOSITORY_ALIAS","UMB_MEDIA_TREE_REPOSITORY_ALIAS","UMB_MEDIA_TREE_STORE_ALIAS","UMB_MEDIA_TREE_ALIAS","UMB_MEDIA_TREE_PICKER_MODAL","UMB_MEDIA_TREE_STORE_CONTEXT","UMB_MEDIA_WORKSPACE_ALIAS","UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD","UMB_MEDIA_WORKSPACE_CONTEXT"] + consts: ["UMB_MEDIA_COLLECTION_ALIAS","UMB_MEDIA_COLLECTION_CONTEXT","UMB_MEDIA_COLLECTION_REPOSITORY_ALIAS","UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS","UMB_MEDIA_TABLE_COLLECTION_VIEW_ALIAS","UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL","UMB_MEDIA_CREATE_OPTIONS_MODAL","UMB_MOVE_MEDIA_REPOSITORY_ALIAS","UMB_SORT_CHILDREN_OF_MEDIA_REPOSITORY_ALIAS","UMB_BULK_MOVE_MEDIA_REPOSITORY_ALIAS","UMB_BULK_TRASH_MEDIA_REPOSITORY_ALIAS","UMB_MEDIA_ENTITY_TYPE","UMB_MEDIA_ROOT_ENTITY_TYPE","UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE","UMB_MEDIA_MENU_ALIAS","UMB_IMAGE_CROPPER_EDITOR_MODAL","UMB_MEDIA_CAPTION_ALT_TEXT_MODAL","UMB_MEDIA_PICKER_MODAL","UMB_MEDIA_WORKSPACE_PATH","UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN","UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN","UMB_MEDIA_VARIANT_CONTEXT","UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE","UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_STORE_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS","UMB_MEDIA_RECYCLE_BIN_TREE_STORE_CONTEXT","UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS","UMB_MEDIA_DETAIL_REPOSITORY_ALIAS","UMB_MEDIA_DETAIL_STORE_ALIAS","UMB_MEDIA_DETAIL_STORE_CONTEXT","UMB_MEDIA_ITEM_REPOSITORY_ALIAS","UMB_MEDIA_STORE_ALIAS","UMB_MEDIA_ITEM_STORE_CONTEXT","UMB_MEDIA_VALIDATION_REPOSITORY_ALIAS","UMB_MEDIA_SEARCH_PROVIDER_ALIAS","UMB_MEDIA_TREE_REPOSITORY_ALIAS","UMB_MEDIA_TREE_STORE_ALIAS","UMB_MEDIA_TREE_ALIAS","UMB_MEDIA_TREE_PICKER_MODAL","UMB_MEDIA_TREE_STORE_CONTEXT","UMB_MEDIA_URL_REPOSITORY_ALIAS","UMB_MEDIA_URL_STORE_ALIAS","UMB_MEDIA_URL_STORE_CONTEXT","UMB_MEDIA_WORKSPACE_ALIAS","UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD","UMB_MEDIA_WORKSPACE_CONTEXT"] }, { path: '@umbraco-cms/backoffice/member-group', @@ -364,7 +364,7 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/temporary-file', - consts: [] + consts: ["UMB_TEMPORARY_FILE_CONFIG_STORE_CONTEXT","UMB_TEMPORARY_FILE_REPOSITORY_ALIAS","UMB_TEMPORARY_FILE_CONFIG_STORE_ALIAS"] }, { path: '@umbraco-cms/backoffice/themes', @@ -404,11 +404,11 @@ export const foundConsts = [{ }, { path: '@umbraco-cms/backoffice/user', - consts: ["UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL","UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS","UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS","UMB_USER_COLLECTION_ALIAS","UMB_USER_COLLECTION_REPOSITORY_ALIAS","UMB_USER_COLLECTION_CONTEXT","UMB_COLLECTION_VIEW_USER_TABLE","UMB_COLLECTION_VIEW_USER_GRID","UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS","UMB_USER_ALLOW_DELETE_CONDITION_ALIAS","UMB_USER_ALLOW_DISABLE_CONDITION_ALIAS","UMB_USER_ALLOW_ENABLE_CONDITION_ALIAS","UMB_USER_ALLOW_EXTERNAL_LOGIN_CONDITION_ALIAS","UMB_USER_ALLOW_MFA_CONDITION_ALIAS","UMB_USER_ALLOW_UNLOCK_CONDITION_ALIAS","UMB_USER_IS_DEFAULT_KIND_CONDITION_ALIAS","UMB_CREATE_USER_MODAL","UMB_CREATE_USER_SUCCESS_MODAL","UMB_CREATE_USER_MODAL_ALIAS","UMB_USER_ENTITY_TYPE","UMB_USER_ROOT_ENTITY_TYPE","UMB_INVITE_USER_MODAL","UMB_RESEND_INVITE_TO_USER_MODAL","UMB_INVITE_USER_REPOSITORY_ALIAS","UMB_USER_MFA_MODAL","UMB_USER_PICKER_MODAL","UMB_USER_WORKSPACE_PATH","UMB_USER_ROOT_WORKSPACE_PATH","UMB_USER_AVATAR_REPOSITORY_ALIAS","UMB_CHANGE_USER_PASSWORD_REPOSITORY_ALIAS","UMB_USER_CONFIG_REPOSITORY_ALIAS","UMB_USER_CONFIG_STORE_ALIAS","UMB_USER_CONFIG_STORE_CONTEXT","UMB_USER_DETAIL_REPOSITORY_ALIAS","UMB_USER_DETAIL_STORE_ALIAS","UMB_USER_DETAIL_STORE_CONTEXT","UMB_DISABLE_USER_REPOSITORY_ALIAS","UMB_ENABLE_USER_REPOSITORY_ALIAS","UMB_USER_ITEM_REPOSITORY_ALIAS","UMB_USER_ITEM_STORE_ALIAS","UMB_USER_ITEM_STORE_CONTEXT","UMB_NEW_USER_PASSWORD_REPOSITORY_ALIAS","UMB_UNLOCK_USER_REPOSITORY_ALIAS","UMB_USER_WORKSPACE_ALIAS","UMB_USER_WORKSPACE_CONTEXT","UMB_USER_ROOT_WORKSPACE_ALIAS"] + consts: ["UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL","UMB_CREATE_USER_CLIENT_CREDENTIAL_MODAL_ALIAS","UMB_USER_CLIENT_CREDENTIAL_REPOSITORY_ALIAS","UMB_USER_COLLECTION_ALIAS","UMB_USER_COLLECTION_REPOSITORY_ALIAS","UMB_USER_COLLECTION_CONTEXT","UMB_COLLECTION_VIEW_USER_TABLE","UMB_COLLECTION_VIEW_USER_GRID","UMB_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS","UMB_CURRENT_USER_ALLOW_CHANGE_PASSWORD_CONDITION_ALIAS","UMB_USER_ALLOW_DELETE_CONDITION_ALIAS","UMB_USER_ALLOW_DISABLE_CONDITION_ALIAS","UMB_USER_ALLOW_ENABLE_CONDITION_ALIAS","UMB_USER_ALLOW_EXTERNAL_LOGIN_CONDITION_ALIAS","UMB_USER_ALLOW_MFA_CONDITION_ALIAS","UMB_CURRENT_USER_ALLOW_MFA_CONDITION_ALIAS","UMB_USER_ALLOW_UNLOCK_CONDITION_ALIAS","UMB_USER_IS_DEFAULT_KIND_CONDITION_ALIAS","UMB_CREATE_USER_MODAL","UMB_CREATE_USER_SUCCESS_MODAL","UMB_CREATE_USER_MODAL_ALIAS","UMB_USER_ENTITY_TYPE","UMB_USER_ROOT_ENTITY_TYPE","UMB_INVITE_USER_MODAL","UMB_RESEND_INVITE_TO_USER_MODAL","UMB_INVITE_USER_REPOSITORY_ALIAS","UMB_USER_MFA_MODAL","UMB_USER_PICKER_MODAL","UMB_USER_WORKSPACE_PATH","UMB_USER_ROOT_WORKSPACE_PATH","UMB_USER_AVATAR_REPOSITORY_ALIAS","UMB_CHANGE_USER_PASSWORD_REPOSITORY_ALIAS","UMB_USER_CONFIG_REPOSITORY_ALIAS","UMB_USER_CONFIG_STORE_ALIAS","UMB_CURRENT_USER_CONFIG_REPOSITORY_ALIAS","UMB_CURRENT_USER_CONFIG_STORE_ALIAS","UMB_CURRENT_USER_CONFIG_STORE_CONTEXT","UMB_USER_CONFIG_STORE_CONTEXT","UMB_USER_DETAIL_REPOSITORY_ALIAS","UMB_USER_DETAIL_STORE_ALIAS","UMB_USER_DETAIL_STORE_CONTEXT","UMB_DISABLE_USER_REPOSITORY_ALIAS","UMB_ENABLE_USER_REPOSITORY_ALIAS","UMB_USER_ITEM_REPOSITORY_ALIAS","UMB_USER_ITEM_STORE_ALIAS","UMB_USER_ITEM_STORE_CONTEXT","UMB_NEW_USER_PASSWORD_REPOSITORY_ALIAS","UMB_UNLOCK_USER_REPOSITORY_ALIAS","UMB_USER_WORKSPACE_ALIAS","UMB_USER_WORKSPACE_CONTEXT","UMB_USER_ROOT_WORKSPACE_ALIAS"] }, { path: '@umbraco-cms/backoffice/utils', - consts: [] + consts: ["UMB_STORAGE_REDIRECT_URL"] }, { path: '@umbraco-cms/backoffice/validation', diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs index d4fd13489b..50b9755162 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Logging; @@ -29,6 +30,7 @@ public class UmbLoginStatusController : SurfaceController => _signInManager = signInManager; [HttpPost] + [AllowAnonymous] [ValidateAntiForgeryToken] [ValidateUmbracoFormRouteString] public async Task HandleLogout([Bind(Prefix = "logoutModel")] PostRedirectModel model) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 7953848ecc..2c89a17466 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,14 +7,14 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^2.0.26", - "@umbraco/playwright-testhelpers": "^15.0.7", + "@umbraco/json-models-builders": "^2.0.27", + "@umbraco/playwright-testhelpers": "^15.0.13", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" }, "devDependencies": { - "@playwright/test": "^1.43", + "@playwright/test": "^1.50", "@types/node": "^20.9.0", "prompt": "^1.2.0", "tslib": "^2.4.0", @@ -32,13 +32,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", - "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", + "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.0" + "playwright": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -58,21 +58,21 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.26.tgz", - "integrity": "sha512-fsIcIcLjT52avK9u+sod6UIR2WlyWMJdlXw+OAePsqLwAXiSuXIEFvLmjfTHzmnw+qOtvKe3olML7e6LPnv/hw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.27.tgz", + "integrity": "sha512-aOBXWc+X1CZlgDs8yBiiD+1rhFdbUzJ0ImmJx0gKjp/qX7T9PNTcZzt/j9DZD49jKbUk7kpeg5Q99wpwlViscg==", "license": "MIT", "dependencies": { "camelize": "^1.0.1" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.7.tgz", - "integrity": "sha512-aJSwU1GDwnMVUp1f8iLYoijDFE8bFiTR07dsYSRXiPoI5tATz8M9LKkfce+e88aaD/5w6HEDAwJtLGaVWp2+fQ==", + "version": "15.0.13", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.13.tgz", + "integrity": "sha512-e9hurtua39vVDD5KnDUOm14TQ2loHGt+/D4af8zpzO/9p/v6NHOj5RFgC5bRYNE5sWoEFIhrJFTkQJW3Jc5DOQ==", "license": "MIT", "dependencies": { - "@umbraco/json-models-builders": "2.0.26", + "@umbraco/json-models-builders": "2.0.27", "node-fetch": "^2.6.7" } }, @@ -189,13 +189,13 @@ } }, "node_modules/playwright": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", - "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", + "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.0" + "playwright-core": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -208,9 +208,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", - "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", + "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 12fe8c1c1e..cfb96b9eac 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -13,15 +13,15 @@ "smokeTestSqlite": "npx playwright test DefaultConfig --grep \"@smoke\" --grep-invert \"Users\"" }, "devDependencies": { - "@playwright/test": "^1.43", + "@playwright/test": "^1.50", "@types/node": "^20.9.0", "prompt": "^1.2.0", "tslib": "^2.4.0", "typescript": "^4.8.3" }, "dependencies": { - "@umbraco/json-models-builders": "^2.0.26", - "@umbraco/playwright-testhelpers": "^15.0.7", + "@umbraco/json-models-builders": "^2.0.27", + "@umbraco/playwright-testhelpers": "^15.0.13", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/User/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/User/User.spec.ts index ed25e99d5d..1be35ac05d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/User/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ApiTesting/User/User.spec.ts @@ -19,7 +19,7 @@ test.describe('User Tests', () => { test('can create a user', async ({umbracoApi}) => { // Act - userId = await umbracoApi.user.createDefaultUser(userName, userEmail, userGroupId); + userId = await umbracoApi.user.createDefaultUser(userName, userEmail, [userGroupId]); // Assert expect(await umbracoApi.user.doesExist(userId)).toBeTruthy(); @@ -28,7 +28,7 @@ test.describe('User Tests', () => { test('can update a user', async ({umbracoApi}) => { // Arrange const anotherUserGroup = await umbracoApi.userGroup.getByName("Translators"); - userId = await umbracoApi.user.createDefaultUser(userName, userEmail, userGroupId); + userId = await umbracoApi.user.createDefaultUser(userName, userEmail, [userGroupId]); const userData = await umbracoApi.user.get(userId); const newUserGroupData = [ userGroupId, @@ -48,7 +48,7 @@ test.describe('User Tests', () => { test('can delete a user', async ({umbracoApi}) => { // Arrange - userId = await umbracoApi.user.createDefaultUser(userName, userEmail, userGroupId); + userId = await umbracoApi.user.createDefaultUser(userName, userEmail, [userGroupId]); expect(await umbracoApi.user.doesExist(userId)).toBeTruthy(); // Act 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 176f19c2b3..f36fb09883 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ChildrenContent.spec.ts @@ -33,8 +33,6 @@ test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) = await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCreateButton(); await umbracoUi.content.chooseDocumentType(childDocumentTypeName); - // This wait is needed - await umbracoUi.waitForTimeout(500); await umbracoUi.content.enterContentName(childContentName); await umbracoUi.content.clickSaveButton(); @@ -74,7 +72,6 @@ test('can create child node in child node', async ({umbracoApi, umbracoUi}) => { await umbracoUi.content.clickCreateButton(); await umbracoUi.content.chooseDocumentType(childOfChildDocumentTypeName); // This wait is needed - await umbracoUi.waitForTimeout(500); await umbracoUi.content.enterContentName(childOfChildContentName); await umbracoUi.content.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts index 9f78c90bda..df67c034ab 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts @@ -34,7 +34,6 @@ test('can see correct information when published', async ({umbracoApi, umbracoUi // Assert await umbracoUi.content.isSuccessNotificationVisible(); - await umbracoUi.waitForTimeout(2000); const contentData = await umbracoApi.document.getByName(contentName); await umbracoUi.content.doesIdHaveText(contentData.id); const expectedCreatedDate = new Date(contentData.variants[0].createDate).toLocaleString("en-US", { @@ -134,8 +133,6 @@ test('cannot change to a template that is not allowed in the document type', asy await umbracoUi.content.clickEditTemplateByName(firstTemplateName); // Assert - // This wait is needed to make sure the template name is visible when the modal is opened - await umbracoUi.waitForTimeout(1000); await umbracoUi.content.isTemplateNameDisabled(secondTemplateName); // Clean diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithApprovedColor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithApprovedColor.spec.ts new file mode 100644 index 0000000000..cfe7aaeb15 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithApprovedColor.spec.ts @@ -0,0 +1,87 @@ +import {AliasHelper, ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Approved Color'; + +test.beforeEach(async ({umbracoApi, umbracoUi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create content with the approved color data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateButton(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.isSuccessNotificationVisible(); + 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([]); +}); + +test('can publish content with the approved color data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + 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([]); +}); + +test('can create content with the custom approved color data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeName = 'CustomApprovedColor'; + const colorValue = 'd73737'; + const colorLabel = 'Test Label'; + const customDataTypeId = await umbracoApi.dataType.createApprovedColorDataTypeWithOneItem(customDataTypeName, colorLabel, colorValue); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.clickApprovedColorByValue(colorValue); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationsHaveCount(2); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); + expect(contentData.values[0].value.label).toEqual(colorLabel); + expect(contentData.values[0].value.value).toEqual('#' + colorValue); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + 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 8ac6b133c8..766b26cdf1 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 @@ -11,7 +11,7 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => { }); test.afterEach(async ({umbracoApi}) => { - await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); @@ -29,7 +29,7 @@ test('can create content with allowed child node enabled', async ({umbracoApi, u await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButton(); - + // Assert await umbracoUi.content.isSuccessNotificationVisible(); expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); @@ -71,8 +71,6 @@ test('can create multiple child nodes with different document types', async ({um await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCreateButton(); await umbracoUi.content.chooseDocumentType(secondChildDocumentTypeName); - // This wait is needed - await umbracoUi.waitForTimeout(500); await umbracoUi.content.enterContentName(secondChildContentName); await umbracoUi.content.clickSaveButton(); @@ -95,4 +93,4 @@ test('can create multiple child nodes with different document types', async ({um await umbracoApi.document.ensureNameNotExists(secondChildContentName); await umbracoApi.documentType.ensureNameNotExists(firstChildDocumentTypeName); await umbracoApi.documentType.ensureNameNotExists(secondChildDocumentTypeName); -}); \ No newline at end of file +}); 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 57af8862bd..17f17d6dab 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 @@ -53,8 +53,6 @@ test('can create child content in a collection', async ({umbracoApi, umbracoUi}) await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCreateButton(); await umbracoUi.content.chooseDocumentType(childDocumentTypeName); - // This wait is needed - await umbracoUi.waitForTimeout(500); await umbracoUi.content.enterContentName(firstChildContentName); await umbracoUi.content.clickSaveButton(); @@ -63,11 +61,9 @@ test('can create child content in a collection', async ({umbracoApi, umbracoUi}) expect(childData.length).toBe(expectedNames.length); expect(childData[0].variants[0].name).toBe(firstChildContentName); // verify that the child content displays in collection list after reloading tree - await umbracoUi.waitForTimeout(1000); await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickReloadButton(); await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.waitForTimeout(500); await umbracoUi.content.doesDocumentTableColumnNameValuesMatch(expectedNames); // Clean @@ -89,8 +85,6 @@ test('can create multiple child nodes in a collection', async ({umbracoApi, umbr await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCreateButton(); await umbracoUi.content.chooseDocumentType(childDocumentTypeName); - // This wait is needed - await umbracoUi.waitForTimeout(500); await umbracoUi.content.enterContentName(secondChildContentName); await umbracoUi.content.clickSaveButton(); @@ -100,11 +94,9 @@ test('can create multiple child nodes in a collection', async ({umbracoApi, umbr expect(childData[0].variants[0].name).toBe(firstChildContentName); expect(childData[1].variants[0].name).toBe(secondChildContentName); // verify that the child content displays in collection list after reloading tree - await umbracoUi.waitForTimeout(1000); await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickReloadButton(); await umbracoUi.content.goToContentWithName(contentName); - await umbracoUi.waitForTimeout(500); await umbracoUi.content.doesDocumentTableColumnNameValuesMatch(expectedNames); // Clean 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 d7aede7a4e..4d042ecb33 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts @@ -75,8 +75,10 @@ test('can publish content with the image cropper data type', {tag: '@smoke'}, as test('can create content with the custom image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange const customDataTypeName = 'CustomImageCropper'; - const cropValue = ['TestCropLabel', 100, 50]; - const customDataTypeId = await umbracoApi.dataType.createImageCropperDataTypeWithOneCrop(customDataTypeName, cropValue[0], cropValue[1], cropValue[2]); + const cropAlias = 'TestCropLabel'; + const cropWidth = 100; + const cropHeight = 50; + const customDataTypeId = await umbracoApi.dataType.createImageCropperDataTypeWithOneCrop(customDataTypeName, AliasHelper.toAlias(cropAlias), cropWidth, cropHeight); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -93,9 +95,9 @@ test('can create content with the custom image cropper data type', {tag: '@smoke expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName)); expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName)); expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint); - expect(contentData.values[0].value.crops[0].alias).toEqual(AliasHelper.toAlias(cropValue[0])); - expect(contentData.values[0].value.crops[0].width).toEqual(cropValue[1]); - expect(contentData.values[0].value.crops[0].height).toEqual(cropValue[2]); + expect(contentData.values[0].value.crops[0].alias).toEqual(AliasHelper.toAlias(cropAlias)); + expect(contentData.values[0].value.crops[0].width).toEqual(cropWidth); + expect(contentData.values[0].value.crops[0].height).toEqual(cropHeight); // Clean await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); 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 eff0c95a69..9e4bb5bb4a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageMediaPicker.spec.ts @@ -73,7 +73,7 @@ test('can add an image to the image media picker', async ({umbracoApi, umbracoUi // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaName); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickSaveButton(); // Assert @@ -168,7 +168,7 @@ test('can add an image from the image media picker with a start node', async ({u // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaName); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickSaveButton(); // Assert 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 ed760248e7..e060a48257 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts @@ -35,7 +35,7 @@ test('can create content with the media picker data type', {tag: '@smoke'}, asyn await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaFileName); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickSaveButton(); // Assert @@ -64,7 +64,7 @@ test('can publish content with the media picker data type', async ({umbracoApi, await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaFileName); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickSaveAndPublishButton(); // Assert 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 cbb71482ae..0cabc0f4ad 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultiURLPicker.spec.ts @@ -37,10 +37,10 @@ test('can create content with the document link', {tag: '@smoke'}, async ({umbra await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickAddMultiURLPickerButton(); - await umbracoUi.content.clickLinkToDocumentButton(); + await umbracoUi.content.clickDocumentLinkButton(); await umbracoUi.content.selectLinkByName(linkedDocumentName); await umbracoUi.content.clickButtonWithName('Choose'); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickAddButton(); await umbracoUi.content.clickSaveButton(); // Assert @@ -72,7 +72,6 @@ test('can publish content with the document link', async ({umbracoApi, umbracoUi const documentTypeForLinkedDocumentId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeForLinkedDocumentName); const linkedDocumentName = 'ContentToPick'; const linkedDocumentId = await umbracoApi.document.createDefaultDocument(linkedDocumentName, documentTypeForLinkedDocumentId); - await umbracoUi.waitForTimeout(2000); await umbracoApi.document.publish(linkedDocumentId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -80,10 +79,10 @@ test('can publish content with the document link', async ({umbracoApi, umbracoUi // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickAddMultiURLPickerButton(); - await umbracoUi.content.clickLinkToDocumentButton(); + await umbracoUi.content.clickDocumentLinkButton(); await umbracoUi.content.selectLinkByName(linkedDocumentName); await umbracoUi.content.clickButtonWithName('Choose'); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickAddButton(); await umbracoUi.content.clickSaveAndPublishButton(); // Assert @@ -115,9 +114,10 @@ test('can create content with the external link', async ({umbracoApi, umbracoUi} // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.clickManualLinkButton(); await umbracoUi.content.enterLink(link); await umbracoUi.content.enterLinkTitle(linkTitle); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickAddButton(); await umbracoUi.content.clickSaveButton(); // Assert @@ -147,11 +147,10 @@ test('can create content with the media link', async ({umbracoApi, umbracoUi}) = // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickAddMultiURLPickerButton(); - await umbracoUi.content.clickLinkToMediaButton(); + await umbracoUi.content.clickMediaLinkButton(); await umbracoUi.content.selectMediaWithName(mediaFileName); - await umbracoUi.content.clickMediaPickerModalSubmitButton(); - await umbracoUi.waitForTimeout(500); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); + await umbracoUi.content.clickAddButton(); await umbracoUi.content.clickSaveButton(); // Assert @@ -185,16 +184,16 @@ test('can add multiple links in the content', async ({umbracoApi, umbracoUi}) => await umbracoUi.content.goToContentWithName(contentName); // Add media link await umbracoUi.content.clickAddMultiURLPickerButton(); - await umbracoUi.content.clickLinkToMediaButton(); + await umbracoUi.content.clickMediaLinkButton(); await umbracoUi.content.selectMediaWithName(mediaFileName); - await umbracoUi.content.clickMediaPickerModalSubmitButton(); - await umbracoUi.waitForTimeout(500); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); + await umbracoUi.content.clickAddButton(); // Add external link await umbracoUi.content.clickAddMultiURLPickerButton(); + await umbracoUi.content.clickManualLinkButton(); await umbracoUi.content.enterLink(link); await umbracoUi.content.enterLinkTitle(linkTitle); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickAddButton(); await umbracoUi.content.clickSaveButton(); // Assert @@ -251,7 +250,7 @@ test('can edit the URL picker in the content', async ({umbracoApi, umbracoUi}) = await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickLinkWithName(linkTitle); await umbracoUi.content.enterLinkTitle(updatedLinkTitle); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickUpdateButton(); await umbracoUi.content.clickSaveButton(); // Assert 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 27255a01cd..42052223c9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleImageMediaPicker.spec.ts @@ -23,13 +23,13 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => { test.afterEach(async ({umbracoApi}) => { await umbracoApi.media.ensureNameNotExists(firstMediaFileName); await umbracoApi.media.ensureNameNotExists(secondMediaFileName); - await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); test('can create content with multiple image media picker data type', async ({umbracoApi, umbracoUi}) => { // Arrange - const expectedState = 'Draft'; + const expectedState = 'Draft'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -51,7 +51,7 @@ test('can create content with multiple image media picker data type', async ({um test('can publish content with multiple image media picker data type', async ({umbracoApi, umbracoUi}) => { // Arrange - const expectedState = 'Published'; + const expectedState = 'Published'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); @@ -81,7 +81,7 @@ test('can add multiple images to the multiple image media picker', async ({umbra await umbracoUi.content.clickChooseMediaPickerButton(); await umbracoUi.content.selectMediaWithName(firstMediaFileName); await umbracoUi.content.selectMediaWithName(secondMediaFileName); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickSaveButton(); // Assert 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 471258ad30..919c1a1ca5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMultipleMediaPicker.spec.ts @@ -24,13 +24,13 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => { test.afterEach(async ({umbracoApi}) => { await umbracoApi.media.ensureNameNotExists(firstMediaFileName); await umbracoApi.media.ensureNameNotExists(secondMediaFileName); - await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.document.ensureNameNotExists(contentName); await umbracoApi.documentType.ensureNameNotExists(documentTypeName); }); test('can create content with multiple media picker data type', async ({umbracoApi, umbracoUi}) => { // Arrange - const expectedState = 'Draft'; + const expectedState = 'Draft'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -52,7 +52,7 @@ test('can create content with multiple media picker data type', async ({umbracoA test('can publish content with multiple media picker data type', async ({umbracoApi, umbracoUi}) => { // Arrange - const expectedState = 'Published'; + const expectedState = 'Published'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); @@ -82,7 +82,7 @@ test('can add multiple media files to the multiple media picker', async ({umbrac await umbracoUi.content.clickChooseMediaPickerButton(); await umbracoUi.content.selectMediaWithName(firstMediaFileName); await umbracoUi.content.selectMediaWithName(secondMediaFileName); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTiptap.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTiptap.spec.ts index 3e94e4c5b7..eff5700a64 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTiptap.spec.ts @@ -13,14 +13,14 @@ test.beforeEach(async ({umbracoApi}) => { }); test.afterEach(async ({umbracoApi}) => { - await umbracoApi.document.ensureNameNotExists(contentName); + 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}) => { // Arrange - const expectedState = 'Draft'; + const expectedState = 'Draft'; await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -42,7 +42,7 @@ test('can create content with empty RTE Tiptap property editor', async ({umbraco test('can create content with non-empty RTE Tiptap property editor', async ({umbracoApi, umbracoUi}) => { // Arrange - const expectedState = 'Draft'; + const expectedState = 'Draft'; const inputText = 'Test Tiptap here'; await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoUi.goToBackOffice(); @@ -101,7 +101,7 @@ test('can add a media in RTE Tiptap property editor', async ({umbracoApi, umbrac await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickTipTapToolbarIconWithTitle(iconTitle); await umbracoUi.content.selectMediaWithName(imageName); - await umbracoUi.content.clickSubmitButton(); + await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickMediaCaptionAltTextModalSubmitButton(); await umbracoUi.content.clickSaveButton(); @@ -140,4 +140,4 @@ test('can add a video in RTE Tiptap property editor', async ({umbracoApi, umbrac const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.values[0].value.markup).toContain('data-embed-url'); expect(contentData.values[0].value.markup).toContain(videoURL); -}); \ No newline at end of file +}); 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 5568e8fe63..42255eddab 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CultureAndHostnames.spec.ts @@ -41,7 +41,6 @@ test('can add a culture', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Assert await umbracoUi.content.isSuccessNotificationVisible(); - await umbracoUi.waitForTimeout(2000); const domainsData = await umbracoApi.document.getDomains(contentId); expect(domainsData.defaultIsoCode).toEqual(isoCode); }); @@ -50,7 +49,6 @@ test('can add a domain', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCultureAndHostnamesButton(); - await umbracoUi.waitForTimeout(1000); await umbracoUi.content.clickAddNewDomainButton(); await umbracoUi.content.enterDomain(domainName); await umbracoUi.content.selectDomainLanguageOption(languageName); @@ -113,7 +111,6 @@ test('can add culture and hostname for multiple languages', async ({umbracoApi, // Act await umbracoUi.content.clickActionsMenuForContent(contentName); await umbracoUi.content.clickCultureAndHostnamesButton(); - await umbracoUi.waitForTimeout(500); await umbracoUi.content.clickAddNewDomainButton(); await umbracoUi.content.enterDomain(domainName, 0); await umbracoUi.content.selectDomainLanguageOption(languageName, 0); @@ -121,7 +118,6 @@ test('can add culture and hostname for multiple languages', async ({umbracoApi, await umbracoUi.content.enterDomain(secondDomainName, 1); await umbracoUi.content.selectDomainLanguageOption(secondLanguageName, 1); await umbracoUi.content.clickSaveModalButton(); - await umbracoUi.waitForTimeout(500); // Assert await umbracoUi.content.isSuccessNotificationVisible(); 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 64ef7b8ec5..d6f14f85af 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,8 +24,8 @@ test('can create a block grid editor', {tag: '@smoke'}, async ({umbracoApi, umbr // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickCreateButton(); - await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickNewDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(blockGridEditorName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); await umbracoUi.dataType.selectAPropertyEditor(blockGridLocatorName); @@ -286,8 +286,6 @@ test('can enable live editing mode in a block grid editor', async ({umbracoApi, // Act await umbracoUi.dataType.goToDataType(blockGridEditorName); - // This wait is currently necessary, sometimes there are issues when clicking the liveEdtingMode button - await umbracoUi.waitForTimeout(2000); await umbracoUi.dataType.clickLiveEditingMode(); await umbracoUi.dataType.clickSaveButton(); @@ -302,8 +300,6 @@ test('can disable live editing mode in a block grid editor', async ({umbracoApi, // Act await umbracoUi.dataType.goToDataType(blockGridEditorName); - // This wait is currently necessary, sometimes there are issues when clicking the liveEditingMode button - await umbracoUi.waitForTimeout(2000); await umbracoUi.dataType.clickLiveEditingMode(); 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 4a96105462..e76d5f17b9 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,8 +24,8 @@ test('can create a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbr // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickCreateButton(); - await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickNewDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(blockListEditorName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); await umbracoUi.dataType.selectAPropertyEditor(blockListLocatorName); 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 12fddb5d0c..e1d277104b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataType.spec.ts @@ -16,8 +16,8 @@ test.afterEach(async ({umbracoApi}) => { test('can create a data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickCreateButton(); - await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickNewDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); await umbracoUi.dataType.selectAPropertyEditor('Text Box'); @@ -86,8 +86,8 @@ 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.clickCreateButton(); - await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickNewDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSaveButton(); 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 ce7983581d..60640bbdb1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/DataTypeFolder.spec.ts @@ -20,7 +20,7 @@ test.afterEach(async ({umbracoApi}) => { test('can create a data type folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.createFolder(dataTypeFolderName); + await umbracoUi.dataType.createDataTypeFolder(dataTypeFolderName); // Assert await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); @@ -37,7 +37,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.clickRenameFolderThreeDotsButton(); await umbracoUi.dataType.enterFolderName(dataTypeFolderName); await umbracoUi.dataType.clickConfirmRenameFolderButton(); @@ -70,8 +70,8 @@ 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.clickCreateButton(); - await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickNewDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(dataTypeName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); await umbracoUi.dataType.selectAPropertyEditor(propertyEditorName); @@ -95,7 +95,7 @@ test('can create a folder in a folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickActionsMenuForDataType(dataTypeFolderName); - await umbracoUi.dataType.createFolder(childFolderName); + await umbracoUi.dataType.createDataTypeFolder(childFolderName); // Assert await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); @@ -116,7 +116,7 @@ test('can create a folder in a folder in a folder', async ({umbracoApi, umbracoU await umbracoUi.dataType.clickRootFolderCaretButton(); await umbracoUi.dataType.clickCaretButtonForName(dataTypeFolderName); await umbracoUi.dataType.clickActionsMenuForDataType(childFolderName); - await umbracoUi.dataType.createFolder(childOfChildFolderName); + await umbracoUi.dataType.createDataTypeFolder(childOfChildFolderName); // Assert await umbracoUi.dataType.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts index 5f09f165fd..6b0c4e2edd 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/ListView.spec.ts @@ -131,7 +131,6 @@ for (const listViewType of listViewTypes) { // Act await umbracoUi.dataType.goToDataType(listViewType); - await umbracoUi.waitForTimeout(500); await umbracoUi.dataType.addLayouts(layoutName); await umbracoUi.dataType.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts index 9e7a82858e..e29b1d2e96 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/MediaPicker.spec.ts @@ -92,14 +92,17 @@ for (const dataTypeName of dataTypes) { // Act await umbracoUi.dataType.goToDataType(dataTypeName); + await umbracoUi.waitForTimeout(500); await umbracoUi.dataType.enterCropValues( cropData[0].toString(), cropData[1].toString(), cropData[2].toString(), cropData[3].toString() ); + await umbracoUi.waitForTimeout(500); await umbracoUi.dataType.clickAddCropButton(); await umbracoUi.dataType.clickSaveButton(); + await umbracoUi.waitForTimeout(500); // Assert dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TinyMCE.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TinyMCE.spec.ts index dc2fd0f372..29e64f1817 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TinyMCE.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/TinyMCE.spec.ts @@ -22,8 +22,8 @@ test('can create a rich text editor with tinyMCE', {tag: '@smoke'}, async ({umbr // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickCreateButton(); - await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickNewDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(tinyMCEName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); await umbracoUi.dataType.selectAPropertyEditor(tinyMCELocatorName, tinyMCEFilterKeyword); 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 0052b1cd75..972f865f08 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Tiptap.spec.ts @@ -21,8 +21,8 @@ test('can create a rich text editor with tiptap', {tag: '@smoke'}, async ({umbra // Act await umbracoUi.dataType.clickActionsMenuAtRoot(); - await umbracoUi.dataType.clickCreateButton(); - await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.clickActionsMenuCreateButton(); + await umbracoUi.dataType.clickNewDataTypeButton(); await umbracoUi.dataType.enterDataTypeName(tipTapName); await umbracoUi.dataType.clickSelectAPropertyEditorButton(); await umbracoUi.dataType.selectAPropertyEditor(tipTapLocatorName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Upload.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Upload.spec.ts index ac90ac8c67..f3eab5c438 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Upload.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/Upload.spec.ts @@ -35,12 +35,15 @@ for (const uploadType of uploadTypes) { await umbracoUi.dataType.goToDataType(uploadType); // Act + await umbracoUi.waitForTimeout(500); await umbracoUi.dataType.clickAddAcceptedFileExtensionsButton(); await umbracoUi.dataType.enterAcceptedFileExtensions(fileExtensionValue); + await umbracoUi.waitForTimeout(500); await umbracoUi.dataType.clickSaveButton(); // Assert await umbracoUi.dataType.isSuccessNotificationVisible(); + await umbracoUi.waitForTimeout(500); dataTypeData = await umbracoApi.dataType.getByName(uploadType); expect(dataTypeData.values).toEqual(expectedDataTypeValues); }); 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 dee91594f8..b18d620f20 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts @@ -60,9 +60,7 @@ test('can create a dictionary item in a dictionary', {tag: '@smoke'}, async ({um // Act await umbracoUi.dictionary.clickActionsMenuForDictionary(parentDictionaryName); - await umbracoUi.waitForTimeout(500); await umbracoUi.dictionary.clickCreateButton(); - await umbracoUi.waitForTimeout(500); await umbracoUi.dictionary.enterDictionaryName(dictionaryName); await umbracoUi.dictionary.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/LogViewer/LogViewer.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/LogViewer/LogViewer.spec.ts index 3173d4b930..66c137903e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/LogViewer/LogViewer.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/LogViewer/LogViewer.spec.ts @@ -189,7 +189,7 @@ test('can use a saved search', async ({umbracoApi, umbracoUi}) => { await umbracoUi.logViewer.goToSettingsTreeItem('Log Viewer'); // Act - await umbracoUi.waitForTimeout(4000); + await umbracoUi.waitForTimeout(2000); await umbracoUi.logViewer.clickSavedSearchByName(searchName); await umbracoUi.logViewer.waitUntilLoadingSpinnerInvisible(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/ListViewMedia.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/ListViewMedia.spec.ts index b841d23cec..c55d98d240 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/ListViewMedia.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/ListViewMedia.spec.ts @@ -33,7 +33,6 @@ test('can change the the default sort order for the list in the media section', await umbracoApi.dataType.updateListViewMediaDataType('orderBy', sortOrder); await umbracoUi.media.goToSection(ConstantHelper.sections.media); await umbracoUi.media.changeToListView(); - await umbracoUi.waitForTimeout(500); // Assert await umbracoUi.media.isMediaListViewVisible(); @@ -52,7 +51,6 @@ test('can change the the order direction for the list in the media section', asy await umbracoUi.media.isMediaGridViewVisible(); await umbracoUi.media.doesMediaGridValuesMatch(expectedMediaValues); await umbracoUi.media.changeToListView(); - await umbracoUi.waitForTimeout(500); await umbracoUi.media.isMediaListViewVisible(); await umbracoUi.media.doesMediaListNameValuesMatch(expectedMediaValues); }); @@ -70,7 +68,6 @@ test('can add more columns to the list in the media section', async ({umbracoApi await umbracoApi.dataType.updateListViewMediaDataType('includeProperties', updatedValue); await umbracoUi.media.goToSection(ConstantHelper.sections.media); await umbracoUi.media.changeToListView(); - await umbracoUi.waitForTimeout(500); // Assert await umbracoUi.media.isMediaListViewVisible(); 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 4eb37f7462..56867a4422 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -39,7 +39,6 @@ test('can rename a media file', async ({umbracoApi, umbracoUi}) => { await umbracoUi.media.goToSection(ConstantHelper.sections.media); // Arrange - await umbracoUi.waitForTimeout(1000); await umbracoUi.media.clickLabelWithName(wrongMediaFileName, true); await umbracoUi.media.enterMediaItemName(mediaFileName); await umbracoUi.media.clickSaveButton(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts index afbe007ac6..c253aac05b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Members/MemberGroups.spec.ts @@ -32,7 +32,7 @@ test('cannot create member group with empty name', async ({umbracoApi, umbracoUi await umbracoUi.memberGroup.clickSaveButton(); // Assert - await umbracoUi.memberGroup.doesErrorNotificationHaveText(NotificationConstantHelper.error.emptyName); + // await umbracoUi.memberGroup.doesErrorNotificationHaveText(NotificationConstantHelper.error.emptyName); expect(await umbracoApi.memberGroup.doesNameExist(memberGroupName)).toBeFalsy(); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/PackagesPackages.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/PackagesPackages.spec.ts index a1b4d192ad..19b4f74a99 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/PackagesPackages.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/PackagesPackages.spec.ts @@ -11,6 +11,5 @@ test.skip('can see the marketplace', async ({umbracoUi}) => { await umbracoUi.package.clickPackagesTab(); // Assert - await umbracoUi.waitForTimeout(2000); await umbracoUi.package.isMarketPlaceIFrameVisible(); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Relation Types/RelationTypes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RelationTypes/RelationTypes.spec.ts similarity index 100% rename from tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Relation Types/RelationTypes.spec.ts rename to tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/RelationTypes/RelationTypes.spec.ts 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 4fb20c7834..ff24ed2416 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 @@ -213,6 +213,7 @@ test('can create a document type with a composition', {tag: '@smoke'}, async ({u // Act await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.waitForTimeout(500); await umbracoUi.documentType.clickCompositionsButton(); await umbracoUi.documentType.clickButtonWithName(compositionDocumentTypeName); await umbracoUi.documentType.clickSubmitButton(); @@ -240,6 +241,7 @@ test('can remove a composition from a document type', async ({umbracoApi, umbrac // Act await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.waitForTimeout(500); await umbracoUi.documentType.clickCompositionsButton(); await umbracoUi.documentType.clickButtonWithName(compositionDocumentTypeName); await umbracoUi.documentType.clickSubmitButton(); @@ -291,7 +293,6 @@ test.skip('can reorder properties in a document type', async ({umbracoApi, umbra await umbracoUi.documentType.goToDocumentType(documentTypeName); await umbracoUi.documentType.clickReorderButton(); // Drag and Drop - await umbracoUi.waitForTimeout(5000); const dragFromLocator = umbracoUi.documentType.getTextLocatorWithName(dataTypeNameTwo); const dragToLocator = umbracoUi.documentType.getTextLocatorWithName(dataTypeName); await umbracoUi.documentType.dragAndDrop(dragFromLocator, dragToLocator, 0, 0, 5); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeSettingsTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeSettingsTab.spec.ts index 22d51bf392..9d220a277f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeSettingsTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeSettingsTab.spec.ts @@ -71,7 +71,6 @@ test('can disable history cleanup for a document type', async ({umbracoApi, umbr // Act await umbracoUi.documentType.goToDocumentType(documentTypeName); // Is needed - await umbracoUi.waitForTimeout(200); await umbracoUi.documentType.clickDocumentTypeSettingsTab(); await umbracoUi.documentType.clickPreventCleanupButton(); await umbracoUi.documentType.clickSaveButton(); @@ -80,4 +79,4 @@ test('can disable history cleanup for a document type', async ({umbracoApi, umbr await umbracoUi.documentType.isSuccessNotificationVisible(); const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); expect(documentTypeData.cleanup.preventCleanup).toBeTruthy(); -}); \ No newline at end of file +}); 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 c372a62dc6..3a07a0d3dd 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 @@ -277,6 +277,7 @@ test('can create a media type with a composition', async ({umbracoApi, umbracoUi // Act await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.waitForTimeout(500); await umbracoUi.mediaType.clickCompositionsButton(); await umbracoUi.mediaType.clickButtonWithName(compositionMediaTypeName); await umbracoUi.mediaType.clickSubmitButton(); 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 571270840e..6edddfed07 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 @@ -43,7 +43,6 @@ test('can create a partial view from snippet', async ({umbracoApi, umbracoUi}) = await umbracoUi.partialView.clickNewPartialViewFromSnippetButton(); await umbracoUi.partialView.clickBreadcrumbButton(); await umbracoUi.partialView.enterPartialViewName(partialViewName); - await umbracoUi.waitForTimeout(500); await umbracoUi.partialView.clickSaveButton(); // Assert 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 2676911ec2..c55ae7af9b 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 @@ -20,7 +20,6 @@ test('can create a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => // Act await umbracoUi.script.clickActionsMenuAtRoot(); await umbracoUi.script.createFolder(scriptFolderName); - await umbracoUi.waitForTimeout(1000); // Assert await umbracoUi.script.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); 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 b314b36b28..0c2f362025 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 @@ -20,7 +20,6 @@ test('can create a folder', async ({umbracoApi, umbracoUi}) => { // Act await umbracoUi.stylesheet.clickActionsMenuAtRoot(); await umbracoUi.stylesheet.createFolder(stylesheetFolderName); - await umbracoUi.waitForTimeout(1000); // Assert await umbracoUi.stylesheet.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created); 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 a76b532995..cfd34fa291 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 @@ -163,7 +163,6 @@ test.skip('can use query builder with Order By statement for a template', async // Act await umbracoUi.template.goToTemplate(templateName); - await umbracoUi.waitForTimeout(1000); await umbracoUi.template.addQueryBuilderWithOrderByStatement(propertyAliasValue, isAscending); // Verify that the code is shown await umbracoUi.template.isQueryBuilderCodeShown(expectedCode); @@ -203,7 +202,6 @@ test('can use query builder with Where statement for a template', async ({umbrac // Act await umbracoUi.template.goToTemplate(templateName); - await umbracoUi.waitForTimeout(500); await umbracoUi.template.addQueryBuilderWithWhereStatement(propertyAliasValue, operatorValue, constrainValue); // Verify that the code is shown await umbracoUi.template.isQueryBuilderCodeShown(expectedCode); @@ -225,7 +223,6 @@ test('can insert sections - render child template into a template', async ({umbr // Act await umbracoUi.template.goToTemplate(templateName); - await umbracoUi.waitForTimeout(1000); await umbracoUi.template.insertSection(sectionType); await umbracoUi.template.clickSaveButton(); @@ -370,7 +367,7 @@ test('cannot create a template with an empty name', {tag: '@smoke'}, async ({umb await umbracoUi.template.clickSaveButton(); // Assert - await umbracoUi.template.isErrorNotificationVisible(); + // await umbracoUi.template.isErrorNotificationVisible(); // TODO: Uncomment this when the front-end updates the error message //await umbracoUi.template.doesErrorNotificationHaveText(NotificationConstantHelper.error.emptyName); expect(await umbracoApi.template.doesNameExist(templateName)).toBeFalsy(); 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 dfabaf39ef..5912e61a02 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 @@ -41,8 +41,6 @@ test('can create content with a rich text editor that has a stylesheet', async ( await umbracoUi.content.clickCreateButton(); await umbracoUi.content.chooseDocumentType(documentTypeName); await umbracoUi.content.enterContentName(documentName); - // Is needed to make sure that the rich text editor is loaded - await umbracoUi.waitForTimeout(500); await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.noAccessToResource, false); await umbracoUi.content.clickSaveButton(); 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 231126b8a9..02089430b4 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 @@ -578,8 +578,6 @@ test('can rollback content with rollback permission enabled', async ({umbracoApi await umbracoUi.content.goToContentWithName(rootDocumentName); await umbracoUi.content.doesDocumentPropertyHaveValue(dataTypeName, updatedTextStringText); await umbracoUi.content.clickInfoTab(); - // Needs to wait for the rollback button to be visible - await umbracoUi.waitForTimeout(500); await umbracoUi.content.clickRollbackButton(); await umbracoUi.content.clickLatestRollBackItem(); await umbracoUi.content.clickRollbackContainerButton(); 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 c3c83d5253..c385b95c8d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -28,7 +28,7 @@ test('can create a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { await umbracoUi.user.enterUserEmail(userEmail); await umbracoUi.user.clickChooseButton(); await umbracoUi.user.clickButtonWithName(defaultUserGroupName); - await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickChooseModalButton(); await umbracoUi.user.clickCreateUserButton(); // Assert @@ -86,7 +86,7 @@ test('can add multiple user groups to a user', async ({umbracoApi, umbracoUi}) = await umbracoUi.user.clickUserWithName(nameOfTheUser); await umbracoUi.user.clickChooseUserGroupsButton(); await umbracoUi.user.clickButtonWithName(secondUserGroupName); - await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickChooseModalButton(); await umbracoUi.user.clickSaveButton(); // Assert @@ -239,7 +239,7 @@ test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi await umbracoUi.user.clickUserWithName(nameOfTheUser); await umbracoUi.user.clickChooseMediaStartNodeButton(); await umbracoUi.user.selectMediaWithName(mediaName); - await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickChooseModalButton(); await umbracoUi.user.clickSaveButton(); // Assert @@ -271,7 +271,7 @@ test('can add multiple media start nodes for a user', async ({umbracoApi, umbrac await umbracoUi.user.clickUserWithName(nameOfTheUser); await umbracoUi.user.clickChooseMediaStartNodeButton(); await umbracoUi.user.selectMediaWithName(secondMediaName); - await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickChooseModalButton(); await umbracoUi.user.clickSaveButton(); // Assert @@ -364,6 +364,8 @@ test('can see if the user has the correct access based on content start nodes', // Act await umbracoUi.user.clickUserWithName(nameOfTheUser); + // Currently this wait is necessary + await umbracoUi.waitForTimeout(2000); // Assert await umbracoUi.user.doesUserHaveAccessToContentNode(documentName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts index 72622cd213..67cd958a48 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts @@ -328,7 +328,7 @@ test('can add a media start node to a user group', async ({umbracoApi, umbracoUi // Act await umbracoUi.userGroup.clickChooseMediaStartNodeButton(); await umbracoUi.userGroup.selectMediaWithName(mediaName); - await umbracoUi.userGroup.clickSubmitButton(); + await umbracoUi.userGroup.clickChooseModalButton(); await umbracoUi.userGroup.clickSaveButton(); // Assert diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/auth.setup.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/auth.setup.ts index 16cae38ed4..a1ac2fa386 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/auth.setup.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/auth.setup.ts @@ -6,11 +6,9 @@ setup('authenticate', async ({page}) => { const umbracoUi = new UiHelpers(page); await umbracoUi.goToBackOffice(); - await page.waitForTimeout(2000); await umbracoUi.login.enterEmail(process.env.UMBRACO_USER_LOGIN); await umbracoUi.login.enterPassword(process.env.UMBRACO_USER_PASSWORD); await umbracoUi.login.clickLoginButton(); - await page.waitForTimeout(2000); await umbracoUi.login.goToSection(ConstantHelper.sections.settings); await umbracoUi.page.context().storageState({path: STORAGE_STATE}); }); diff --git a/tests/Umbraco.Tests.Common/TestHelpers/DocumentUpdateHelper.cs b/tests/Umbraco.Tests.Common/TestHelpers/DocumentUpdateHelper.cs new file mode 100644 index 0000000000..25873a8dca --- /dev/null +++ b/tests/Umbraco.Tests.Common/TestHelpers/DocumentUpdateHelper.cs @@ -0,0 +1,59 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Common.TestHelpers; + +public static class DocumentUpdateHelper +{ + public static UpdateDocumentRequestModel CreateInvariantDocumentUpdateRequestModel(ContentCreateModel createModel) + { + var updateRequestModel = new UpdateDocumentRequestModel(); + + updateRequestModel.Template = ReferenceByIdModel.ReferenceOrNull(createModel.TemplateKey); + updateRequestModel.Variants = + [ + new DocumentVariantRequestModel + { + Segment = null, + Culture = null, + Name = createModel.InvariantName!, + } + ]; + updateRequestModel.Values = createModel.InvariantProperties.Select(x => new DocumentValueModel + { + Alias = x.Alias, + Value = x.Value, + }); + + return updateRequestModel; + } + + public static CreateDocumentRequestModel CreateDocumentRequestModel(ContentCreateModel createModel) + { + var createDocumentRequestModel = new CreateDocumentRequestModel + { + Template = ReferenceByIdModel.ReferenceOrNull(createModel.TemplateKey), + DocumentType = new ReferenceByIdModel(createModel.ContentTypeKey), + Parent = ReferenceByIdModel.ReferenceOrNull(createModel.ParentKey), + }; + + createDocumentRequestModel.Variants = + [ + new DocumentVariantRequestModel + { + Segment = null, + Culture = null, + Name = createModel.InvariantName!, + } + ]; + createDocumentRequestModel.Values = createModel.InvariantProperties.Select(x => new DocumentValueModel + { + Alias = x.Alias, + Value = x.Value, + }); + + + return createDocumentRequestModel; + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs b/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs index 043ed9a13a..ab03d0e402 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs @@ -47,41 +47,56 @@ public abstract class ManagementApiTest : UmbracoTestServerTestBase protected virtual string Url => GetManagementApiUrl(MethodSelector); - protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin) + protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin) => + await AuthenticateClientAsync(client, + async userService => + { + IUser user; + if (isAdmin) + { + user = await userService.GetRequiredUserAsync(Constants.Security.SuperUserKey); + user.Username = user.Email = username; + userService.Save(user); + } + else + { + user = (await userService.CreateAsync( + Constants.Security.SuperUserKey, + new UserCreateModel + { + Email = username, + Name = username, + UserName = username, + UserGroupKeys = new HashSet(new[] { Constants.Security.EditorGroupKey }) + }, + true)).Result.CreatedUser; + } + + return (user, password); + }); + + + protected async Task AuthenticateClientAsync(HttpClient client, Func> createUser) { - Guid userKey = Constants.Security.SuperUserKey; + OpenIddictApplicationDescriptor backofficeOpenIddictApplicationDescriptor; var scopeProvider = GetRequiredService(); + + string? username; + string? password; + using (var scope = scopeProvider.CreateCoreScope()) { var userService = GetRequiredService(); using var serviceScope = GetRequiredService().CreateScope(); var userManager = serviceScope.ServiceProvider.GetRequiredService(); - IUser user; - if (isAdmin) - { - user = await userService.GetRequiredUserAsync(userKey); - user.Username = user.Email = username; - userService.Save(user); - } - else - { - user = (await userService.CreateAsync( - Constants.Security.SuperUserKey, - new UserCreateModel() - { - Email = username, - Name = username, - UserName = username, - UserGroupKeys = new HashSet(new[] { Constants.Security.EditorGroupKey }) - }, - true)).Result.CreatedUser; - userKey = user.Key; - } + var userCreationResult = await createUser(userService); + username = userCreationResult.user.Username; + password = userCreationResult.Password; + var userKey = userCreationResult.user.Key; - - var token = await userManager.GeneratePasswordResetTokenAsync(user); + var token = await userManager.GeneratePasswordResetTokenAsync(userCreationResult.user); var changePasswordAttempt = await userService.ChangePasswordAsync(userKey, @@ -99,6 +114,7 @@ public abstract class ManagementApiTest : UmbracoTestServerTestBase BackOfficeApplicationManager; backofficeOpenIddictApplicationDescriptor = backOfficeApplicationManager.BackofficeOpenIddictApplicationDescriptor(client.BaseAddress); + scope.Complete(); } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Policies/CreateDocumentTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Policies/CreateDocumentTests.cs new file mode 100644 index 0000000000..bf59619f65 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Policies/CreateDocumentTests.cs @@ -0,0 +1,130 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Document; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.TestHelpers; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Policies; + +[TestFixture] +public class CreateDocumentTests : ManagementApiTest +{ + private IUserGroupService UserGroupService => GetRequiredService(); + + private IShortStringHelper ShortStringHelper => GetRequiredService(); + + private ITemplateService TemplateService => GetRequiredService(); + + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + protected override Expression> MethodSelector => + x => x.Create(CancellationToken.None, null!); + + [Test] + public async Task ReadonlyUserCannotCreateDocument() + { + var userGroup = await CreateReadonlyUserGroupAsync(); + + await AuthenticateClientAsync(Client, async userService => + { + var email = "test@test.com"; + var testUserCreateModel = new UserCreateModel + { + Email = email, + Name = "Test Mc.Gee", + UserName = email, + UserGroupKeys = new HashSet { userGroup.Key }, + }; + + var userCreationResult = + await userService.CreateAsync(Constants.Security.SuperUserKey, testUserCreateModel, true); + + Assert.IsTrue(userCreationResult.Success); + + return (userCreationResult.Result.CreatedUser, "1234567890"); + }); + + var (contentType, template) = await CreateDocumentTypeAsync(); + var contentCreateModel = ContentEditingBuilder.CreateSimpleContent(contentType.Key); + + var requestModel = DocumentUpdateHelper.CreateDocumentRequestModel(contentCreateModel); + + var response = await GetManagementApiResponseAsync(requestModel); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden)); + } + + [Test] + public async Task EditorCanCreateDocument() + { + await AuthenticateClientAsync(Client, "editor@editor.com", "1234567890", false); + + var (contentType, template) = await CreateDocumentTypeAsync(); + var requestModel = DocumentUpdateHelper.CreateDocumentRequestModel(ContentEditingBuilder.CreateSimpleContent(contentType.Key)); + + var response = await GetManagementApiResponseAsync(requestModel); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Created)); + + var locationHeader = response.Headers.GetValues("Location").First(); + var key = Guid.Parse(locationHeader.Split('/')[^1]); + var createdContent = ContentService.GetById(key); + Assert.NotNull(createdContent); + } + + private async Task GetManagementApiResponseAsync(CreateDocumentRequestModel requestModel) + { + var url = GetManagementApiUrl(x => x.Create(CancellationToken.None, requestModel)); + var requestBody = new StringContent(JsonSerializer.Serialize(requestModel), Encoding.UTF8, "application/json"); + var response = await Client.PostAsync(url, requestBody); + return response; + } + + private async Task CreateReadonlyUserGroupAsync() + { + var userGroup = new UserGroup(ShortStringHelper) + { + Name = "Test", + Alias = "test", + Permissions = new HashSet { ActionBrowse.ActionLetter }, + HasAccessToAllLanguages = true, + StartContentId = -1, + StartMediaId = -1 + }; + userGroup.AddAllowedSection("content"); + userGroup.AddAllowedSection("media"); + + var groupCreationResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + Assert.IsTrue(groupCreationResult.Success); + return groupCreationResult.Result; + } + + private async Task<(IContentType contentType, ITemplate template)> CreateDocumentTypeAsync() + { + var userKey = Constants.Security.SuperUserKey; + var template = TemplateBuilder.CreateTextPageTemplate(); + var templateAttempt = await TemplateService.CreateAsync(template, userKey); + Assert.IsTrue(templateAttempt.Success); + + var contentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType(defaultTemplateKey: template.Key); + var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(contentTypeCreateModel, userKey); + Assert.IsTrue(contentTypeAttempt.Success); + + return (contentTypeAttempt.Result!, templateAttempt.Result!); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Policies/UpdateDocumentTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Policies/UpdateDocumentTests.cs new file mode 100644 index 0000000000..b7d15f2fd2 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Policies/UpdateDocumentTests.cs @@ -0,0 +1,153 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Document; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.TestHelpers; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Policies; + +public class UpdateDocumentTests : ManagementApiTest +{ + private IUserGroupService UserGroupService => GetRequiredService(); + + private IShortStringHelper ShortStringHelper => GetRequiredService(); + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + private ITemplateService TemplateService => GetRequiredService(); + + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + private IContentPublishingService ContentPublishingService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + protected override Expression> MethodSelector => + x => x.Update(CancellationToken.None, Guid.Empty, null!); + + [Test] + public async Task UserWithoutPermissionCannotUpdate() + { + var userGroup = new UserGroup(ShortStringHelper) + { + Name = "Test", + Alias = "test", + Permissions = new HashSet { ActionBrowse.ActionLetter }, + HasAccessToAllLanguages = true, + StartContentId = -1, + StartMediaId = -1 + }; + userGroup.AddAllowedSection("content"); + userGroup.AddAllowedSection("media"); + + var groupCreationResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + Assert.IsTrue(groupCreationResult.Success); + + await AuthenticateClientAsync(Client, async userService => + { + var email = "test@test.com"; + var testUserCreateModel = new UserCreateModel + { + Email = email, + Name = "Test Mc.Gee", + UserName = email, + UserGroupKeys = new HashSet { groupCreationResult.Result.Key }, + }; + + var userCreationResult = + await userService.CreateAsync(Constants.Security.SuperUserKey, testUserCreateModel, true); + + Assert.IsTrue(userCreationResult.Success); + + return (userCreationResult.Result.CreatedUser, "1234567890"); + }); + + const string UpdatedName = "NewName"; + + var model = await CreateContent(); + var updateRequestModel = CreateRequestModel(model, UpdatedName); + + var response = await GetManagementApiResponse(model, updateRequestModel); + + AssertResponse(response, model, HttpStatusCode.Forbidden, model.InvariantName); + } + + [Test] + public async Task UserWithPermissionCanUpdate() + { + // "Default" version creates an editor that has permission to update content. + await AuthenticateClientAsync(Client, "editor@editor.com", "1234567890", false); + + const string UpdatedName = "NewName"; + + var model = await CreateContent(); + var updateRequestModel = CreateRequestModel(model, UpdatedName); + + var response = await GetManagementApiResponse(model, updateRequestModel); + + AssertResponse(response, model, HttpStatusCode.OK, UpdatedName); + } + + private async Task CreateContent() + { + var userKey = Constants.Security.SuperUserKey; + var template = TemplateBuilder.CreateTextPageTemplate(); + var templateAttempt = await TemplateService.CreateAsync(template, userKey); + Assert.IsTrue(templateAttempt.Success); + + var contentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType(defaultTemplateKey: template.Key); + var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(contentTypeCreateModel, userKey); + Assert.IsTrue(contentTypeAttempt.Success); + + var textPage = ContentEditingBuilder.CreateSimpleContent(contentTypeAttempt.Result.Key); + textPage.TemplateKey = templateAttempt.Result.Key; + textPage.Key = Guid.NewGuid(); + var createContentResult = await ContentEditingService.CreateAsync(textPage, userKey); + Assert.IsTrue(createContentResult.Success); + + var publishResult = await ContentPublishingService.PublishAsync( + createContentResult.Result.Content!.Key, + [new() { Culture = "*" }], + userKey); + + Assert.IsTrue(publishResult.Success); + return textPage; + } + + private static UpdateDocumentRequestModel CreateRequestModel(ContentCreateModel model, string name) + { + var updateRequestModel = DocumentUpdateHelper.CreateInvariantDocumentUpdateRequestModel(model); + updateRequestModel.Variants.First().Name = name; + return updateRequestModel; + } + + private async Task GetManagementApiResponse(ContentCreateModel model, UpdateDocumentRequestModel updateRequestModel) + { + var url = GetManagementApiUrl(x => x.Update(CancellationToken.None, model.Key!.Value, null)); + var requestBody = new StringContent(JsonSerializer.Serialize(updateRequestModel), Encoding.UTF8, "application/json"); + return await Client.PutAsync(url, requestBody); + } + + private void AssertResponse(HttpResponseMessage response, ContentCreateModel model, HttpStatusCode expectedStatusCode, string expectedContentName) + { + Assert.That(response.StatusCode, Is.EqualTo(expectedStatusCode)); + var content = ContentService.GetById(model.Key!.Value); + Assert.IsNotNull(content); + Assert.That(content.Name, Is.EqualTo(expectedContentName)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs new file mode 100644 index 0000000000..704b28c6aa --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs @@ -0,0 +1,2486 @@ +using System.Text.Json.Nodes; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Umbraco.Cms.Api.Delivery.Controllers; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Tests.Integration.TestServerTest; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi; + +[TestFixture] +public class OpenApiContractTest : UmbracoTestServerTestBase +{ + private GlobalSettings GlobalSettings => GetRequiredService>().Value; + + private IHostingEnvironment HostingEnvironment => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + => builder.AddMvcAndRazor(mvcBuilder => + { + // Adds Umbraco.Cms.Api.Delivery + mvcBuilder.AddApplicationPart(typeof(DeliveryApiControllerBase).Assembly); + }); + + [Test] + public async Task Validate_OpenApi_Contract() + { + var backOfficePath = GlobalSettings.GetBackOfficePath(HostingEnvironment); + + var swaggerPath = $"{backOfficePath}/swagger/delivery/swagger.json"; + + var generatedOpenApiContract = await Client.GetStringAsync(swaggerPath); + var generatedOpenApiJson = JsonNode.Parse(generatedOpenApiContract); + var expectedOpenApiJson = JsonNode.Parse(ExpectedOpenApiContract); + + Assert.NotNull(generatedOpenApiJson); + Assert.NotNull(expectedOpenApiJson); + + Assert.AreEqual(expectedOpenApiJson.ToJsonString(), generatedOpenApiJson.ToJsonString(), $"Generated API do not respect the contract."); + } + + private const string ExpectedOpenApiContract = + """ + { + "openapi": "3.0.1", + "info": { + "title": "Umbraco Delivery API", + "description": "You can find out more about the Umbraco Delivery API in [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api).", + "version": "Latest" + }, + "paths": { + "/umbraco/delivery/api/v1/content": { + "get": { + "tags": [ + "Content" + ], + "operationId": "GetContent", + "parameters": [ + { + "name": "fetch", + "in": "query", + "description": "Specifies the content items to fetch. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Select all": { + "value": "" + }, + "Select all ancestors of a node by id": { + "value": "ancestors:id" + }, + "Select all ancestors of a node by path": { + "value": "ancestors:path" + }, + "Select all children of a node by id": { + "value": "children:id" + }, + "Select all children of a node by path": { + "value": "children:path" + }, + "Select all descendants of a node by id": { + "value": "descendants:id" + }, + "Select all descendants of a node by path": { + "value": "descendants:path" + } + } + }, + { + "name": "filter", + "in": "query", + "description": "Defines how to filter the fetched content items. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Default filter": { + "value": "" + }, + "Filter by content type (equals)": { + "value": [ + "contentType:alias1" + ] + }, + "Filter by name (contains)": { + "value": [ + "name:nodeName" + ] + }, + "Filter by creation date (less than)": { + "value": [ + "createDate<2024-01-01" + ] + }, + "Filter by update date (greater than or equal)": { + "value": [ + "updateDate>:2023-01-01" + ] + } + } + }, + { + "name": "sort", + "in": "query", + "description": "Defines how to sort the found content items. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Default sort": { + "value": "" + }, + "Sort by create date": { + "value": [ + "createDate:asc", + "createDate:desc" + ] + }, + "Sort by level": { + "value": [ + "level:asc", + "level:desc" + ] + }, + "Sort by name": { + "value": [ + "name:asc", + "name:desc" + ] + }, + "Sort by sort order": { + "value": [ + "sortOrder:asc", + "sortOrder:desc" + ] + }, + "Sort by update date": { + "value": [ + "updateDate:asc", + "updateDate:desc" + ] + } + } + }, + { + "name": "skip", + "in": "query", + "description": "Specifies the number of found content items to skip. Use this to control pagination of the response.", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "description": "Specifies the number of found content items to take. Use this to control pagination of the response.", + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all": { + "value": "all" + }, + "Expand specific property": { + "value": "property:alias1" + }, + "Expand specific properties": { + "value": "property:alias1,alias2" + } + } + }, + { + "name": "Accept-Language", + "in": "header", + "description": "Defines the language to return. Use this when querying language variant content items.", + "schema": { + "type": "string" + }, + "examples": { + "Default": { + "value": "" + }, + "English culture": { + "value": "en-us" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + }, + { + "name": "Preview", + "in": "header", + "description": "Whether to request draft content.", + "schema": { + "type": "boolean" + } + }, + { + "name": "Start-Item", + "in": "header", + "description": "URL segment or GUID of a root content item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedIApiContentResponseModel" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": true + } + }, + "/umbraco/delivery/api/v2/content": { + "get": { + "tags": [ + "Content" + ], + "operationId": "GetContent2.0", + "parameters": [ + { + "name": "fetch", + "in": "query", + "description": "Specifies the content items to fetch. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Select all": { + "value": "" + }, + "Select all ancestors of a node by id": { + "value": "ancestors:id" + }, + "Select all ancestors of a node by path": { + "value": "ancestors:path" + }, + "Select all children of a node by id": { + "value": "children:id" + }, + "Select all children of a node by path": { + "value": "children:path" + }, + "Select all descendants of a node by id": { + "value": "descendants:id" + }, + "Select all descendants of a node by path": { + "value": "descendants:path" + } + } + }, + { + "name": "filter", + "in": "query", + "description": "Defines how to filter the fetched content items. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Default filter": { + "value": "" + }, + "Filter by content type (equals)": { + "value": [ + "contentType:alias1" + ] + }, + "Filter by name (contains)": { + "value": [ + "name:nodeName" + ] + }, + "Filter by creation date (less than)": { + "value": [ + "createDate<2024-01-01" + ] + }, + "Filter by update date (greater than or equal)": { + "value": [ + "updateDate>:2023-01-01" + ] + } + } + }, + { + "name": "sort", + "in": "query", + "description": "Defines how to sort the found content items. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Default sort": { + "value": "" + }, + "Sort by create date": { + "value": [ + "createDate:asc", + "createDate:desc" + ] + }, + "Sort by level": { + "value": [ + "level:asc", + "level:desc" + ] + }, + "Sort by name": { + "value": [ + "name:asc", + "name:desc" + ] + }, + "Sort by sort order": { + "value": [ + "sortOrder:asc", + "sortOrder:desc" + ] + }, + "Sort by update date": { + "value": [ + "updateDate:asc", + "updateDate:desc" + ] + } + } + }, + { + "name": "skip", + "in": "query", + "description": "Specifies the number of found content items to skip. Use this to control pagination of the response.", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "description": "Specifies the number of found content items to take. Use this to control pagination of the response.", + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all properties": { + "value": "properties[$all]" + }, + "Expand specific property": { + "value": "properties[alias1]" + }, + "Expand specific properties": { + "value": "properties[alias1,alias2]" + }, + "Expand nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Explicitly defines which properties should be included in the response (by default all properties are included). Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Include all properties": { + "value": "properties[$all]" + }, + "Include only specific property": { + "value": "properties[alias1]" + }, + "Include only specific properties": { + "value": "properties[alias1,alias2]" + }, + "Include only specific nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "Accept-Language", + "in": "header", + "description": "Defines the language to return. Use this when querying language variant content items.", + "schema": { + "type": "string" + }, + "examples": { + "Default": { + "value": "" + }, + "English culture": { + "value": "en-us" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + }, + { + "name": "Preview", + "in": "header", + "description": "Whether to request draft content.", + "schema": { + "type": "boolean" + } + }, + { + "name": "Start-Item", + "in": "header", + "description": "URL segment or GUID of a root content item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedIApiContentResponseModel" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/umbraco/delivery/api/v1/content/item": { + "get": { + "tags": [ + "Content" + ], + "operationId": "GetContentItem", + "parameters": [ + { + "name": "id", + "in": "query", + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all": { + "value": "all" + }, + "Expand specific property": { + "value": "property:alias1" + }, + "Expand specific properties": { + "value": "property:alias1,alias2" + } + } + }, + { + "name": "Accept-Language", + "in": "header", + "description": "Defines the language to return. Use this when querying language variant content items.", + "schema": { + "type": "string" + }, + "examples": { + "Default": { + "value": "" + }, + "English culture": { + "value": "en-us" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + }, + { + "name": "Preview", + "in": "header", + "description": "Whether to request draft content.", + "schema": { + "type": "boolean" + } + }, + { + "name": "Start-Item", + "in": "header", + "description": "URL segment or GUID of a root content item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentResponseModel" + } + ] + } + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + }, + "deprecated": true + } + }, + "/umbraco/delivery/api/v1/content/item/{path}": { + "get": { + "tags": [ + "Content" + ], + "operationId": "GetContentItemByPath", + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all": { + "value": "all" + }, + "Expand specific property": { + "value": "property:alias1" + }, + "Expand specific properties": { + "value": "property:alias1,alias2" + } + } + }, + { + "name": "Accept-Language", + "in": "header", + "description": "Defines the language to return. Use this when querying language variant content items.", + "schema": { + "type": "string" + }, + "examples": { + "Default": { + "value": "" + }, + "English culture": { + "value": "en-us" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + }, + { + "name": "Preview", + "in": "header", + "description": "Whether to request draft content.", + "schema": { + "type": "boolean" + } + }, + { + "name": "Start-Item", + "in": "header", + "description": "URL segment or GUID of a root content item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": true + } + }, + "/umbraco/delivery/api/v2/content/item/{path}": { + "get": { + "tags": [ + "Content" + ], + "operationId": "GetContentItemByPath2.0", + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all properties": { + "value": "properties[$all]" + }, + "Expand specific property": { + "value": "properties[alias1]" + }, + "Expand specific properties": { + "value": "properties[alias1,alias2]" + }, + "Expand nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Explicitly defines which properties should be included in the response (by default all properties are included). Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Include all properties": { + "value": "properties[$all]" + }, + "Include only specific property": { + "value": "properties[alias1]" + }, + "Include only specific properties": { + "value": "properties[alias1,alias2]" + }, + "Include only specific nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "Accept-Language", + "in": "header", + "description": "Defines the language to return. Use this when querying language variant content items.", + "schema": { + "type": "string" + }, + "examples": { + "Default": { + "value": "" + }, + "English culture": { + "value": "en-us" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + }, + { + "name": "Preview", + "in": "header", + "description": "Whether to request draft content.", + "schema": { + "type": "boolean" + } + }, + { + "name": "Start-Item", + "in": "header", + "description": "URL segment or GUID of a root content item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/umbraco/delivery/api/v1/content/item/{id}": { + "get": { + "tags": [ + "Content" + ], + "operationId": "GetContentItemById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all": { + "value": "all" + }, + "Expand specific property": { + "value": "property:alias1" + }, + "Expand specific properties": { + "value": "property:alias1,alias2" + } + } + }, + { + "name": "Accept-Language", + "in": "header", + "description": "Defines the language to return. Use this when querying language variant content items.", + "schema": { + "type": "string" + }, + "examples": { + "Default": { + "value": "" + }, + "English culture": { + "value": "en-us" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + }, + { + "name": "Preview", + "in": "header", + "description": "Whether to request draft content.", + "schema": { + "type": "boolean" + } + }, + { + "name": "Start-Item", + "in": "header", + "description": "URL segment or GUID of a root content item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": true + } + }, + "/umbraco/delivery/api/v2/content/item/{id}": { + "get": { + "tags": [ + "Content" + ], + "operationId": "GetContentItemById2.0", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all properties": { + "value": "properties[$all]" + }, + "Expand specific property": { + "value": "properties[alias1]" + }, + "Expand specific properties": { + "value": "properties[alias1,alias2]" + }, + "Expand nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Explicitly defines which properties should be included in the response (by default all properties are included). Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Include all properties": { + "value": "properties[$all]" + }, + "Include only specific property": { + "value": "properties[alias1]" + }, + "Include only specific properties": { + "value": "properties[alias1,alias2]" + }, + "Include only specific nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "Accept-Language", + "in": "header", + "description": "Defines the language to return. Use this when querying language variant content items.", + "schema": { + "type": "string" + }, + "examples": { + "Default": { + "value": "" + }, + "English culture": { + "value": "en-us" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + }, + { + "name": "Preview", + "in": "header", + "description": "Whether to request draft content.", + "schema": { + "type": "boolean" + } + }, + { + "name": "Start-Item", + "in": "header", + "description": "URL segment or GUID of a root content item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/umbraco/delivery/api/v2/content/items": { + "get": { + "tags": [ + "Content" + ], + "operationId": "GetContentItems2.0", + "parameters": [ + { + "name": "id", + "in": "query", + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all properties": { + "value": "properties[$all]" + }, + "Expand specific property": { + "value": "properties[alias1]" + }, + "Expand specific properties": { + "value": "properties[alias1,alias2]" + }, + "Expand nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Explicitly defines which properties should be included in the response (by default all properties are included). Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Include all properties": { + "value": "properties[$all]" + }, + "Include only specific property": { + "value": "properties[alias1]" + }, + "Include only specific properties": { + "value": "properties[alias1,alias2]" + }, + "Include only specific nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "Accept-Language", + "in": "header", + "description": "Defines the language to return. Use this when querying language variant content items.", + "schema": { + "type": "string" + }, + "examples": { + "Default": { + "value": "" + }, + "English culture": { + "value": "en-us" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + }, + { + "name": "Preview", + "in": "header", + "description": "Whether to request draft content.", + "schema": { + "type": "boolean" + } + }, + { + "name": "Start-Item", + "in": "header", + "description": "URL segment or GUID of a root content item.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentResponseModel" + } + ] + } + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + } + } + } + }, + "/umbraco/delivery/api/v1/media": { + "get": { + "tags": [ + "Media" + ], + "operationId": "GetMedia", + "parameters": [ + { + "name": "fetch", + "in": "query", + "description": "Specifies the media items to fetch. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Select all children at root level": { + "value": "children:/" + }, + "Select all children of a media item by id": { + "value": "children:id" + }, + "Select all children of a media item by path": { + "value": "children:path" + } + } + }, + { + "name": "filter", + "in": "query", + "description": "Defines how to filter the fetched media items. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Default filter": { + "value": "" + }, + "Filter by media type": { + "value": [ + "mediaType:alias1" + ] + }, + "Filter by name": { + "value": [ + "name:nodeName" + ] + } + } + }, + { + "name": "sort", + "in": "query", + "description": "Defines how to sort the found media items. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Default sort": { + "value": "" + }, + "Sort by create date": { + "value": [ + "createDate:asc", + "createDate:desc" + ] + }, + "Sort by name": { + "value": [ + "name:asc", + "name:desc" + ] + }, + "Sort by sort order": { + "value": [ + "sortOrder:asc", + "sortOrder:desc" + ] + }, + "Sort by update date": { + "value": [ + "updateDate:asc", + "updateDate:desc" + ] + } + } + }, + { + "name": "skip", + "in": "query", + "description": "Specifies the number of found media items to skip. Use this to control pagination of the response.", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "description": "Specifies the number of found media items to take. Use this to control pagination of the response.", + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all": { + "value": "all" + }, + "Expand specific property": { + "value": "property:alias1" + }, + "Expand specific properties": { + "value": "property:alias1,alias2" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedIApiMediaWithCropsResponseModel" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + } + }, + "deprecated": true + } + }, + "/umbraco/delivery/api/v2/media": { + "get": { + "tags": [ + "Media" + ], + "operationId": "GetMedia2.0", + "parameters": [ + { + "name": "fetch", + "in": "query", + "description": "Specifies the media items to fetch. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Select all children at root level": { + "value": "children:/" + }, + "Select all children of a media item by id": { + "value": "children:id" + }, + "Select all children of a media item by path": { + "value": "children:path" + } + } + }, + { + "name": "filter", + "in": "query", + "description": "Defines how to filter the fetched media items. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Default filter": { + "value": "" + }, + "Filter by media type": { + "value": [ + "mediaType:alias1" + ] + }, + "Filter by name": { + "value": [ + "name:nodeName" + ] + } + } + }, + { + "name": "sort", + "in": "query", + "description": "Defines how to sort the found media items. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Default sort": { + "value": "" + }, + "Sort by create date": { + "value": [ + "createDate:asc", + "createDate:desc" + ] + }, + "Sort by name": { + "value": [ + "name:asc", + "name:desc" + ] + }, + "Sort by sort order": { + "value": [ + "sortOrder:asc", + "sortOrder:desc" + ] + }, + "Sort by update date": { + "value": [ + "updateDate:asc", + "updateDate:desc" + ] + } + } + }, + { + "name": "skip", + "in": "query", + "description": "Specifies the number of found media items to skip. Use this to control pagination of the response.", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "description": "Specifies the number of found media items to take. Use this to control pagination of the response.", + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all properties": { + "value": "properties[$all]" + }, + "Expand specific property": { + "value": "properties[alias1]" + }, + "Expand specific properties": { + "value": "properties[alias1,alias2]" + }, + "Expand nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Explicitly defines which properties should be included in the response (by default all properties are included). Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Include all properties": { + "value": "properties[$all]" + }, + "Include only specific property": { + "value": "properties[alias1]" + }, + "Include only specific properties": { + "value": "properties[alias1,alias2]" + }, + "Include only specific nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedIApiMediaWithCropsResponseModel" + } + ] + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + } + } + } + }, + "/umbraco/delivery/api/v1/media/item": { + "get": { + "tags": [ + "Media" + ], + "operationId": "GetMediaItem", + "parameters": [ + { + "name": "id", + "in": "query", + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all": { + "value": "all" + }, + "Expand specific property": { + "value": "property:alias1" + }, + "Expand specific properties": { + "value": "property:alias1,alias2" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiMediaWithCropsResponseModel" + } + ] + } + } + } + } + } + }, + "deprecated": true + } + }, + "/umbraco/delivery/api/v1/media/item/{path}": { + "get": { + "tags": [ + "Media" + ], + "operationId": "GetMediaItemByPath", + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all": { + "value": "all" + }, + "Expand specific property": { + "value": "property:alias1" + }, + "Expand specific properties": { + "value": "property:alias1,alias2" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiMediaWithCropsResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": true + } + }, + "/umbraco/delivery/api/v2/media/item/{path}": { + "get": { + "tags": [ + "Media" + ], + "operationId": "GetMediaItemByPath2.0", + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all properties": { + "value": "properties[$all]" + }, + "Expand specific property": { + "value": "properties[alias1]" + }, + "Expand specific properties": { + "value": "properties[alias1,alias2]" + }, + "Expand nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Explicitly defines which properties should be included in the response (by default all properties are included). Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Include all properties": { + "value": "properties[$all]" + }, + "Include only specific property": { + "value": "properties[alias1]" + }, + "Include only specific properties": { + "value": "properties[alias1,alias2]" + }, + "Include only specific nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiMediaWithCropsResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/umbraco/delivery/api/v1/media/item/{id}": { + "get": { + "tags": [ + "Media" + ], + "operationId": "GetMediaItemById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all": { + "value": "all" + }, + "Expand specific property": { + "value": "property:alias1" + }, + "Expand specific properties": { + "value": "property:alias1,alias2" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiMediaWithCropsResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": true + } + }, + "/umbraco/delivery/api/v2/media/item/{id}": { + "get": { + "tags": [ + "Media" + ], + "operationId": "GetMediaItemById2.0", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all properties": { + "value": "properties[$all]" + }, + "Expand specific property": { + "value": "properties[alias1]" + }, + "Expand specific properties": { + "value": "properties[alias1,alias2]" + }, + "Expand nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Explicitly defines which properties should be included in the response (by default all properties are included). Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Include all properties": { + "value": "properties[$all]" + }, + "Include only specific property": { + "value": "properties[alias1]" + }, + "Include only specific properties": { + "value": "properties[alias1,alias2]" + }, + "Include only specific nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiMediaWithCropsResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/umbraco/delivery/api/v2/media/items": { + "get": { + "tags": [ + "Media" + ], + "operationId": "GetMediaItems2.0", + "parameters": [ + { + "name": "id", + "in": "query", + "schema": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + }, + { + "name": "expand", + "in": "query", + "description": "Defines the properties that should be expanded in the response. Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Expand none": { + "value": "" + }, + "Expand all properties": { + "value": "properties[$all]" + }, + "Expand specific property": { + "value": "properties[alias1]" + }, + "Expand specific properties": { + "value": "properties[alias1,alias2]" + }, + "Expand nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "fields", + "in": "query", + "description": "Explicitly defines which properties should be included in the response (by default all properties are included). Refer to [the documentation](https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/media-delivery-api#query-parameters) for more details on this.", + "schema": { + "type": "string" + }, + "examples": { + "Include all properties": { + "value": "properties[$all]" + }, + "Include only specific property": { + "value": "properties[alias1]" + }, + "Include only specific properties": { + "value": "properties[alias1,alias2]" + }, + "Include only specific nested properties": { + "value": "properties[alias1[properties[nestedAlias1,nestedAlias2]]]" + } + } + }, + { + "name": "Api-Key", + "in": "header", + "description": "API key specified through configuration to authorize access to the API.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiMediaWithCropsResponseModel" + } + ] + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ApiContentResponseModel": { + "required": [ + "contentType", + "createDate", + "cultures", + "id", + "name", + "properties", + "route", + "updateDate" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "contentType": { + "type": "string" + }, + "properties": { + "type": "object", + "additionalProperties": { + "nullable": true + } + }, + "name": { + "type": "string" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "updateDate": { + "type": "string", + "format": "date-time" + }, + "route": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentRouteModel" + } + ] + }, + "cultures": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentRouteModel" + } + ] + } + } + }, + "additionalProperties": false + }, + "ApiContentRouteModel": { + "required": [ + "path", + "startItem" + ], + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "startItem": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentStartItemModel" + } + ] + } + }, + "additionalProperties": false + }, + "ApiContentStartItemModel": { + "required": [ + "id", + "path" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "path": { + "type": "string" + } + }, + "additionalProperties": false + }, + "ApiMediaWithCropsResponseModel": { + "required": [ + "createDate", + "id", + "mediaType", + "name", + "path", + "properties", + "updateDate", + "url" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "name": { + "type": "string", + "readOnly": true + }, + "mediaType": { + "type": "string", + "readOnly": true + }, + "url": { + "type": "string", + "readOnly": true + }, + "extension": { + "type": "string", + "nullable": true, + "readOnly": true + }, + "width": { + "type": "integer", + "format": "int32", + "nullable": true, + "readOnly": true + }, + "height": { + "type": "integer", + "format": "int32", + "nullable": true, + "readOnly": true + }, + "bytes": { + "type": "integer", + "format": "int32", + "nullable": true, + "readOnly": true + }, + "properties": { + "type": "object", + "additionalProperties": { + "nullable": true + }, + "readOnly": true + }, + "focalPoint": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImageFocalPointModel" + } + ], + "nullable": true + }, + "crops": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImageCropModel" + } + ] + }, + "nullable": true + }, + "path": { + "type": "string" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "updateDate": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, + "ImageCropCoordinatesModel": { + "required": [ + "x1", + "x2", + "y1", + "y2" + ], + "type": "object", + "properties": { + "x1": { + "type": "number", + "format": "double" + }, + "y1": { + "type": "number", + "format": "double" + }, + "x2": { + "type": "number", + "format": "double" + }, + "y2": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "ImageCropModel": { + "required": [ + "height", + "width" + ], + "type": "object", + "properties": { + "alias": { + "type": "string", + "nullable": true + }, + "width": { + "type": "integer", + "format": "int32" + }, + "height": { + "type": "integer", + "format": "int32" + }, + "coordinates": { + "oneOf": [ + { + "$ref": "#/components/schemas/ImageCropCoordinatesModel" + } + ], + "nullable": true + } + }, + "additionalProperties": false + }, + "ImageFocalPointModel": { + "required": [ + "left", + "top" + ], + "type": "object", + "properties": { + "left": { + "type": "number", + "format": "double" + }, + "top": { + "type": "number", + "format": "double" + } + }, + "additionalProperties": false + }, + "PagedIApiContentResponseModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiContentResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, + "PagedIApiMediaWithCropsResponseModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiMediaWithCropsResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, + "ProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": { } + } + } + } + } + """; +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs index 46a983e435..500b7bdc0d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs @@ -213,44 +213,6 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); } - [Test] - public async Task Two_items_in_level_1_with_same_name_will_have_conflicting_routes() - { - // Create a second root - var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); - ContentService.Save(secondRoot, -1, contentSchedule); - - // Create a child of second root - var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, Subpage.Name, secondRoot); - childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); - ContentService.Save(childOfSecondRoot, -1, contentSchedule); - - // Publish both the main root and the second root with descendants - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); - - var subPageUrls = await DocumentUrlService.ListUrlsAsync(Subpage.Key); - var childOfSecondRootUrls = await DocumentUrlService.ListUrlsAsync(childOfSecondRoot.Key); - - //Assert the url of subpage is correct - Assert.AreEqual(1, subPageUrls.Count()); - Assert.IsTrue(subPageUrls.First().IsUrl); - Assert.AreEqual("/text-page-1", subPageUrls.First().Text); - Assert.AreEqual(Subpage.Key, DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, false)); - - //Assert the url of child of second root is not exposed - Assert.AreEqual(1, childOfSecondRootUrls.Count()); - Assert.IsFalse(childOfSecondRootUrls.First().IsUrl); - - //Ensure the url without hide top level is not finding the child of second root - Assert.AreNotEqual(childOfSecondRoot.Key, DocumentUrlService.GetDocumentKeyByRoute("/second-root/text-page-1", "en-US", null, false)); - - - - } - - //TODO test cases: // - Find the root, when a domain is set diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs index 1944a114fc..41e3f18979 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest_hidetoplevel_false.cs @@ -120,33 +120,4 @@ public class DocumentUrlServiceTest_HideTopLevel_False : UmbracoIntegrationTestW return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper(); } - - [Test] - public async Task Two_items_in_level_1_with_same_name_will_not_have_conflicting_routes() - { - // Create a second root - var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); - ContentService.Save(secondRoot, -1, contentSchedule); - - // Create a child of second root - var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, Subpage.Name, secondRoot); - childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); - ContentService.Save(childOfSecondRoot, -1, contentSchedule); - - // Publish both the main root and the second root with descendants - ContentService.PublishBranch(Textpage, true, new[] { "*" }); - ContentService.PublishBranch(secondRoot, true, new[] { "*" }); - - var subPageUrls = await DocumentUrlService.ListUrlsAsync(Subpage.Key); - var childOfSecondRootUrls = await DocumentUrlService.ListUrlsAsync(childOfSecondRoot.Key); - - Assert.AreEqual(1, subPageUrls.Count()); - Assert.IsTrue(subPageUrls.First().IsUrl); - Assert.AreEqual("/textpage/text-page-1", subPageUrls.First().Text); - - Assert.AreEqual(1, childOfSecondRootUrls.Count()); - Assert.IsTrue(childOfSecondRootUrls.First().IsUrl); - Assert.AreEqual("/second-root/text-page-1", childOfSecondRootUrls.First().Text); - } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs new file mode 100644 index 0000000000..0b73743b8a --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public class PublishedUrlInfoProviderTests : PublishedUrlInfoProviderTestsBase +{ + + [Test] + public async Task Two_items_in_level_1_with_same_name_will_have_conflicting_routes() + { + // Create a second root + var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(secondRoot, -1, contentSchedule); + + // Create a child of second root + var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, Subpage.Name, secondRoot); + childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); + ContentService.Save(childOfSecondRoot, -1, contentSchedule); + + // Publish both the main root and the second root with descendants + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + + var subPageUrls = await PublishedUrlInfoProvider.GetAllAsync(Subpage); + var childOfSecondRootUrls = await PublishedUrlInfoProvider.GetAllAsync(childOfSecondRoot); + + // Assert the url of subpage is correct + Assert.AreEqual(1, subPageUrls.Count); + Assert.IsTrue(subPageUrls.First().IsUrl); + Assert.AreEqual("/text-page-1/", subPageUrls.First().Text); + Assert.AreEqual(Subpage.Key, DocumentUrlService.GetDocumentKeyByRoute("/text-page-1/", "en-US", null, false)); + + // Assert the url of child of second root is not exposed + Assert.AreEqual(1, childOfSecondRootUrls.Count); + Assert.IsFalse(childOfSecondRootUrls.First().IsUrl); + + // Ensure the url without hide top level is not finding the child of second root + Assert.AreNotEqual(childOfSecondRoot.Key, DocumentUrlService.GetDocumentKeyByRoute("/second-root/text-page-1/", "en-US", null, false)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTestsBase.cs new file mode 100644 index 0000000000..1a27d998af --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTestsBase.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Tests.Common; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Mock)] +public abstract class PublishedUrlInfoProviderTestsBase : UmbracoIntegrationTestWithContent +{ + protected IDocumentUrlService DocumentUrlService => GetRequiredService(); + + protected IPublishedUrlInfoProvider PublishedUrlInfoProvider => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddUnique(); + builder.AddNotificationHandler(); + builder.Services.AddNotificationAsyncHandler(); + builder.Services.AddUnique(serviceProvider => new TestUmbracoContextAccessor(GetUmbracoContext(serviceProvider))); + builder.Services.AddUnique(CreateHttpContextAccessor()); + } + + public override void Setup() + { + DocumentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); + base.Setup(); + } + + private IUmbracoContext GetUmbracoContext(IServiceProvider serviceProvider) + { + var mock = new Mock(); + + mock.Setup(x => x.Content).Returns(serviceProvider.GetRequiredService()); + mock.Setup(x => x.CleanedUmbracoUrl).Returns(new Uri("https://localhost:44339")); + + return mock.Object; + } + + private IHttpContextAccessor CreateHttpContextAccessor() + { + var mock = new Mock(); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "https"; + httpContext.Request.Host = new HostString("localhost"); + + mock.Setup(x => x.HttpContext).Returns(httpContext); + return mock.Object; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs new file mode 100644 index 0000000000..b0f1a03ec1 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public class PublishedUrlInfoProvider_hidetoplevel_false : PublishedUrlInfoProviderTestsBase +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.Configure(x => x.HideTopLevelNodeFromPath = false); + base.CustomTestSetup(builder); + } + + [Test] + public async Task Two_items_in_level_1_with_same_name_will_not_have_conflicting_routes() + { + // Create a second root + var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + ContentService.Save(secondRoot, -1, contentSchedule); + + // Create a child of second root + var childOfSecondRoot = ContentBuilder.CreateSimpleContent(ContentType, Subpage.Name, secondRoot); + childOfSecondRoot.Key = new Guid("FF6654FB-BC68-4A65-8C6C-135567F50BD6"); + ContentService.Save(childOfSecondRoot, -1, contentSchedule); + + // Publish both the main root and the second root with descendants + ContentService.PublishBranch(Textpage, true, new[] { "*" }); + ContentService.PublishBranch(secondRoot, true, new[] { "*" }); + + var subPageUrls = await PublishedUrlInfoProvider.GetAllAsync(Subpage); + var childOfSecondRootUrls = await PublishedUrlInfoProvider.GetAllAsync(childOfSecondRoot); + + Assert.AreEqual(1, subPageUrls.Count); + Assert.IsTrue(subPageUrls.First().IsUrl); + Assert.AreEqual("/textpage/text-page-1/", subPageUrls.First().Text); + + Assert.AreEqual(1, childOfSecondRootUrls.Count); + Assert.IsTrue(childOfSecondRootUrls.First().IsUrl); + Assert.AreEqual("/second-root/text-page-1/", childOfSecondRootUrls.First().Text); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs index 3313d83cd3..7df133a3ba 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs @@ -78,6 +78,8 @@ public class BackOfficeExamineSearcherTests : ExamineBaseTest pageSize, pageIndex, out _, + null, + null, ignoreUserStartNodes: true); private async Task SetupUserIdentity(string userId) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Copy.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Copy.cs index 364b60674f..6695f7eedf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Copy.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Copy.cs @@ -198,6 +198,48 @@ public partial class ContentEditingServiceTests } } + [TestCase(true)] + [TestCase(false)] + public async Task Can_Copy_Onto_Self(bool includeDescendants) + { + var contentType = await CreateTextPageContentTypeAsync(); + (IContent root, IContent child) = await CreateRootAndChildAsync(contentType); + + var result = await ContentEditingService.CopyAsync(root.Key, root.Key, false, includeDescendants, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + + VerifyCopy(result.Result); + + // re-get and re-test + VerifyCopy(await ContentEditingService.GetAsync(result.Result!.Key)); + + void VerifyCopy(IContent? copiedRoot) + { + Assert.IsNotNull(copiedRoot); + Assert.AreEqual(root.Id, copiedRoot.ParentId); + Assert.IsTrue(copiedRoot.HasIdentity); + Assert.AreNotEqual(root.Key, copiedRoot.Key); + Assert.AreEqual(root.Name, copiedRoot.Name); + var copiedChildren = ContentService.GetPagedChildren(copiedRoot.Id, 0, 100, out var total).ToArray(); + + if (includeDescendants) + { + Assert.AreEqual(1, copiedChildren.Length); + Assert.AreEqual(1, total); + var copiedChild = copiedChildren.First(); + Assert.AreNotEqual(child.Id, copiedChild.Id); + Assert.AreNotEqual(child.Key, copiedChild.Key); + Assert.AreEqual(child.Name, copiedChild.Name); + } + else + { + Assert.AreEqual(0, copiedChildren.Length); + Assert.AreEqual(0, total); + } + } + } + [Test] public async Task Can_Relate_Copy_To_Original() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs index 63fafd7716..a36c08bebd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs @@ -54,7 +54,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler(); private void CreateTestData() { @@ -176,6 +177,69 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest } } + [Test] + public void Publishing_Invariant() + { + IContent document = new Content("content", -1, _contentType); + ContentService.Save(document); + + var treeChangeWasCalled = false; + + ContentNotificationHandler.TreeChange += notification => + { + var change = notification.Changes.FirstOrDefault(); + var publishedCultures = change?.PublishedCultures?.ToArray(); + Assert.IsNotNull(publishedCultures); + Assert.AreEqual(1, publishedCultures.Length); + Assert.IsTrue(publishedCultures.InvariantContains("*")); + Assert.IsNull(change.UnpublishedCultures); + + treeChangeWasCalled = true; + }; + + try + { + ContentService.Publish(document, ["*"]); + Assert.IsTrue(treeChangeWasCalled); + } + finally + { + ContentNotificationHandler.TreeChange = null; + } + } + + [Test] + public void Unpublishing_Invariant() + { + IContent document = new Content("content", -1, _contentType); + ContentService.Save(document); + ContentService.Publish(document, ["*"]); + + var treeChangeWasCalled = false; + + ContentNotificationHandler.TreeChange += notification => + { + var change = notification.Changes.FirstOrDefault(); + Assert.IsNull(change?.PublishedCultures); + var unpublishedCultures = change?.UnpublishedCultures?.ToArray(); + Assert.IsNotNull(unpublishedCultures); + Assert.AreEqual(1, unpublishedCultures.Length); + Assert.IsTrue(unpublishedCultures.InvariantContains("*")); + + treeChangeWasCalled = true; + }; + + try + { + ContentService.Unpublish(document); + Assert.IsTrue(treeChangeWasCalled); + } + finally + { + ContentNotificationHandler.TreeChange = null; + } + } + [Test] public async Task Publishing_Culture() { @@ -202,6 +266,7 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest var publishingWasCalled = false; var publishedWasCalled = false; + var treeChangeWasCalled = false; ContentNotificationHandler.PublishingContent += notification => { @@ -227,16 +292,30 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest publishedWasCalled = true; }; + ContentNotificationHandler.TreeChange += notification => + { + var change = notification.Changes.FirstOrDefault(); + var publishedCultures = change?.PublishedCultures?.ToArray(); + Assert.IsNotNull(publishedCultures); + Assert.AreEqual(1, publishedCultures.Length); + Assert.IsTrue(publishedCultures.InvariantContains("fr-FR")); + Assert.IsNull(change.UnpublishedCultures); + + treeChangeWasCalled = true; + }; + try { ContentService.Publish(document, new[] { "fr-FR" }); Assert.IsTrue(publishingWasCalled); Assert.IsTrue(publishedWasCalled); + Assert.IsTrue(treeChangeWasCalled); } finally { ContentNotificationHandler.PublishingContent = null; ContentNotificationHandler.PublishedContent = null; + ContentNotificationHandler.TreeChange = null; } document = ContentService.GetById(document.Id); @@ -399,6 +478,7 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest var publishingWasCalled = false; var publishedWasCalled = false; + var treeChangeWasCalled = false; // TODO: revisit this - it was migrated when removing static events, but the expected result seems illogic - why does this test bind to Published and not Unpublished? @@ -432,16 +512,30 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest publishedWasCalled = true; }; + ContentNotificationHandler.TreeChange += notification => + { + var change = notification.Changes.FirstOrDefault(); + var unpublishedCultures = change?.UnpublishedCultures?.ToArray(); + Assert.IsNotNull(unpublishedCultures); + Assert.AreEqual(1, unpublishedCultures.Length); + Assert.IsTrue(unpublishedCultures.InvariantContains("fr-FR")); + Assert.IsNull(change.PublishedCultures); + + treeChangeWasCalled = true; + }; + try { ContentService.CommitDocumentChanges(document); Assert.IsTrue(publishingWasCalled); Assert.IsTrue(publishedWasCalled); + Assert.IsTrue(treeChangeWasCalled); } finally { ContentNotificationHandler.PublishingContent = null; ContentNotificationHandler.PublishedContent = null; + ContentNotificationHandler.TreeChange = null; } document = ContentService.GetById(document.Id); @@ -456,7 +550,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler { public static Action SavingContent { get; set; } @@ -470,6 +565,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest public static Action UnpublishedContent { get; set; } + public static Action TreeChange { get; set; } + public void Handle(ContentPublishedNotification notification) => PublishedContent?.Invoke(notification); public void Handle(ContentPublishingNotification notification) => PublishingContent?.Invoke(notification); @@ -480,5 +577,7 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest public void Handle(ContentUnpublishedNotification notification) => UnpublishedContent?.Invoke(notification); public void Handle(ContentUnpublishingNotification notification) => UnpublishingContent?.Invoke(notification); + + public void Handle(ContentTreeChangeNotification notification) => TreeChange?.Invoke(notification); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 50d67af360..9efd93a4bc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -247,5 +247,11 @@ MediaNavigationServiceTests.cs + + PublishedUrlInfoProviderTestsBase.cs + + + PublishedUrlInfoProviderTestsBase.cs + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Json/DeliveryApiVersionAwareJsonConverterBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Json/DeliveryApiVersionAwareJsonConverterBaseTests.cs new file mode 100644 index 0000000000..53c1b66fc2 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Json/DeliveryApiVersionAwareJsonConverterBaseTests.cs @@ -0,0 +1,212 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Api.Delivery.Json; +using Umbraco.Cms.Core.DeliveryApi; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Delivery.Json; + +[TestFixture] +public class DeliveryApiVersionAwareJsonConverterBaseTests +{ + private Mock _httpContextAccessorMock; + private Mock _apiVersioningFeatureMock; + + private void SetUpMocks(int apiVersion) + { + _httpContextAccessorMock = new Mock(); + _apiVersioningFeatureMock = new Mock(); + + _apiVersioningFeatureMock + .SetupGet(feature => feature.RequestedApiVersion) + .Returns(new ApiVersion(apiVersion)); + + var httpContext = new DefaultHttpContext(); + httpContext.Features.Set(_apiVersioningFeatureMock.Object); + + _httpContextAccessorMock + .SetupGet(accessor => accessor.HttpContext) + .Returns(httpContext); + } + + [Test] + [TestCase(1, new[] { "PropertyAll", "PropertyV1Max", "PropertyV2Max", "PropertyV2Only", "PropertyV2Min" })] + [TestCase(2, new[] { "PropertyAll", "PropertyV1Max", "PropertyV2Max", "PropertyV2Only", "PropertyV2Min" })] + [TestCase(3, new[] { "PropertyAll", "PropertyV1Max", "PropertyV2Max", "PropertyV2Only", "PropertyV2Min" })] + public void Can_Include_All_Properties_When_HttpContext_Is_Not_Available(int apiVersion, string[] expectedPropertyNames) + { + // Arrange + using var memoryStream = new MemoryStream(); + using var jsonWriter = new Utf8JsonWriter(memoryStream); + + _httpContextAccessorMock = new Mock(); + _apiVersioningFeatureMock = new Mock(); + + _apiVersioningFeatureMock + .SetupGet(feature => feature.RequestedApiVersion) + .Returns(new ApiVersion(apiVersion)); + + _httpContextAccessorMock + .SetupGet(accessor => accessor.HttpContext) + .Returns((HttpContext)null); + + var sut = new TestJsonConverter(_httpContextAccessorMock.Object); + + // Act + sut.Write(jsonWriter, new TestResponseModel(), new JsonSerializerOptions()); + jsonWriter.Flush(); + + memoryStream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(memoryStream); + var output = reader.ReadToEnd(); + + // Assert + Assert.That(expectedPropertyNames.All(v => output.Contains(v, StringComparison.InvariantCulture)), Is.True); + } + + [Test] + [TestCase(1, new[] { "PropertyAll", "PropertyV1Max", "PropertyV2Max" }, new[] { "PropertyV2Min", "PropertyV2Only" })] + [TestCase(2, new[] { "PropertyAll", "PropertyV2Min", "PropertyV2Only", "PropertyV2Max" }, new[] { "PropertyV1Max" })] + [TestCase(3, new[] { "PropertyAll", "PropertyV2Min" }, new[] { "PropertyV1Max", "PropertyV2Only", "PropertyV2Max" })] + public void Can_Include_Correct_Properties_Based_On_Version_Attribute(int apiVersion, string[] expectedPropertyNames, string[] expectedDisallowedPropertyNames) + { + var jsonOptions = new JsonSerializerOptions(); + var output = GetJsonOutput(apiVersion, jsonOptions); + + // Assert + Assert.Multiple(() => + { + Assert.That(expectedPropertyNames.All(v => output.Contains(v, StringComparison.InvariantCulture)), Is.True); + Assert.That(expectedDisallowedPropertyNames.All(v => output.Contains(v, StringComparison.InvariantCulture) is false), Is.True); + }); + } + + [Test] + [TestCase(1, new[] { "PropertyAll", "PropertyV1Max", "PropertyV2Max" })] + [TestCase(2, new[] { "PropertyAll", "PropertyV2Min", "PropertyV2Only", "PropertyV2Max" })] + [TestCase(3, new[] { "PropertyAll", "PropertyV2Min" })] + public void Can_Serialize_Properties_Correctly_Based_On_Version_Attribute(int apiVersion, string[] expectedPropertyNames) + { + var jsonOptions = new JsonSerializerOptions(); + var output = GetJsonOutput(apiVersion, jsonOptions); + + // Verify values correspond to properties + var jsonDoc = JsonDocument.Parse(output); + var root = jsonDoc.RootElement; + + // Assert + foreach (var propertyName in expectedPropertyNames) + { + var expectedValue = GetPropertyValue(propertyName); + Assert.AreEqual(expectedValue, root.GetProperty(propertyName).GetString()); + } + } + + [Test] + [TestCase(1, new[] { "propertyAll", "propertyV1Max", "propertyV2Max" }, new[] { "propertyV2Min", "propertyV2Only" })] + [TestCase(2, new[] { "propertyAll", "propertyV2Min", "propertyV2Only", "propertyV2Max" }, new[] { "propertyV1Max" })] + [TestCase(3, new[] { "propertyAll", "propertyV2Min" }, new[] { "propertyV1Max", "propertyV2Only", "propertyV2Max" })] + public void Can_Respect_Property_Naming_Policy_On_Json_Options(int apiVersion, string[] expectedPropertyNames, string[] expectedDisallowedPropertyNames) + { + // Set up CamelCase naming policy + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + var output = GetJsonOutput(apiVersion, jsonOptions); + + // Assert + Assert.Multiple(() => + { + Assert.That(expectedPropertyNames.All(v => output.Contains(v, StringComparison.InvariantCulture)), Is.True); + Assert.That(expectedDisallowedPropertyNames.All(v => output.Contains(v, StringComparison.InvariantCulture) is false), Is.True); + }); + } + + [Test] + [TestCase(1, "PropertyV1Max", "PropertyAll")] + [TestCase(2, "PropertyV2Min", "PropertyAll")] + public void Can_Respect_Property_Order(int apiVersion, string expectedFirstPropertyName, string expectedLastPropertyName) + { + var jsonOptions = new JsonSerializerOptions(); + var output = GetJsonOutput(apiVersion, jsonOptions); + + // Parse the JSON to verify the order of properties + using var jsonDocument = JsonDocument.Parse(output); + var rootElement = jsonDocument.RootElement; + + var properties = rootElement.EnumerateObject().ToList(); + var firstProperty = properties.First(); + var lastProperty = properties.Last(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(expectedFirstPropertyName, firstProperty.Name); + Assert.AreEqual(expectedLastPropertyName, lastProperty.Name); + }); + } + + private string GetJsonOutput(int apiVersion, JsonSerializerOptions jsonOptions) + { + // Arrange + using var memoryStream = new MemoryStream(); + using var jsonWriter = new Utf8JsonWriter(memoryStream); + + SetUpMocks(apiVersion); + var sut = new TestJsonConverter(_httpContextAccessorMock.Object); + + // Act + sut.Write(jsonWriter, new TestResponseModel(), jsonOptions); + jsonWriter.Flush(); + + memoryStream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(memoryStream); + + return reader.ReadToEnd(); + } + + private string GetPropertyValue(string propertyName) + { + var model = new TestResponseModel(); + return propertyName switch + { + nameof(TestResponseModel.PropertyAll) => model.PropertyAll, + nameof(TestResponseModel.PropertyV1Max) => model.PropertyV1Max, + nameof(TestResponseModel.PropertyV2Max) => model.PropertyV2Max, + nameof(TestResponseModel.PropertyV2Min) => model.PropertyV2Min, + nameof(TestResponseModel.PropertyV2Only) => model.PropertyV2Only, + _ => throw new ArgumentException($"Unknown property name: {propertyName}"), + }; + } +} + +internal class TestJsonConverter : DeliveryApiVersionAwareJsonConverterBase +{ + public TestJsonConverter(IHttpContextAccessor httpContextAccessor) + : base(httpContextAccessor) + { + } +} + +internal class TestResponseModel +{ + [JsonPropertyOrder(100)] + public string PropertyAll { get; init; } = "all"; + + [IncludeInApiVersion(maxVersion: 1)] + public string PropertyV1Max { get; init; } = "v1"; + + [IncludeInApiVersion(2)] + public string PropertyV2Min { get; init; } = "v2+"; + + [IncludeInApiVersion(2, 2)] + public string PropertyV2Only { get; init; } = "v2"; + + [IncludeInApiVersion(maxVersion: 2)] + public string PropertyV2Max { get; init; } = "up to v2"; +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs index 182e0f8e9c..3f00ac1e5e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs @@ -27,26 +27,68 @@ public class RefresherTests Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); } - [Test] - public void ContentCacheRefresherCanDeserializeJsonPayload() + [TestCase(TreeChangeTypes.None, false)] + [TestCase(TreeChangeTypes.RefreshAll, true)] + [TestCase(TreeChangeTypes.RefreshBranch, false)] + [TestCase(TreeChangeTypes.Remove, true)] + [TestCase(TreeChangeTypes.RefreshNode, false)] + public void ContentCacheRefresherCanDeserializeJsonPayload(TreeChangeTypes changeTypes, bool blueprint) { + var key = Guid.NewGuid(); ContentCacheRefresher.JsonPayload[] source = { new ContentCacheRefresher.JsonPayload() { Id = 1234, - Key = Guid.NewGuid(), - ChangeTypes = TreeChangeTypes.None + Key = key, + ChangeTypes = changeTypes, + Blueprint = blueprint } }; var json = JsonSerializer.Serialize(source); var payload = JsonSerializer.Deserialize(json); - Assert.AreEqual(source[0].Id, payload[0].Id); - Assert.AreEqual(source[0].Key, payload[0].Key); - Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); - Assert.AreEqual(source[0].Blueprint, payload[0].Blueprint); + Assert.AreEqual(1234, payload[0].Id); + Assert.AreEqual(key, payload[0].Key); + Assert.AreEqual(changeTypes, payload[0].ChangeTypes); + Assert.AreEqual(blueprint, payload[0].Blueprint); + Assert.IsNull(payload[0].PublishedCultures); + Assert.IsNull(payload[0].UnpublishedCultures); + } + + [Test] + public void ContentCacheRefresherCanDeserializeJsonPayloadWithCultures() + { + var key = Guid.NewGuid(); + ContentCacheRefresher.JsonPayload[] source = + { + new ContentCacheRefresher.JsonPayload() + { + Id = 1234, + Key = key, + PublishedCultures = ["en-US", "da-DK"], + UnpublishedCultures = ["de-DE"] + } + }; + + var json = JsonSerializer.Serialize(source); + var payload = JsonSerializer.Deserialize(json); + + Assert.IsNotNull(payload[0].PublishedCultures); + Assert.Multiple(() => + { + Assert.AreEqual(2, payload[0].PublishedCultures.Length); + Assert.AreEqual("en-US", payload[0].PublishedCultures.First()); + Assert.AreEqual("da-DK", payload[0].PublishedCultures.Last()); + }); + + Assert.IsNotNull(payload[0].UnpublishedCultures); + Assert.Multiple(() => + { + Assert.AreEqual(1, payload[0].UnpublishedCultures.Length); + Assert.AreEqual("de-DE", payload[0].UnpublishedCultures.First()); + }); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiDocumentUrlServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiDocumentUrlServiceTests.cs new file mode 100644 index 0000000000..a79bf59941 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiDocumentUrlServiceTests.cs @@ -0,0 +1,46 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; + +[TestFixture] +public class ApiDocumentUrlServiceTests +{ + [Test] + public void Can_Resolve_Document_Key_With_Start_Node() + { + var documentKey = Guid.NewGuid(); + var documentUrlServiceMock = new Mock(); + documentUrlServiceMock + .Setup(m => m.GetDocumentKeyByRoute( + "/some/where", + It.IsAny(), + 1234, + false)) + .Returns(documentKey); + + var apiDocumentUrlService = new ApiDocumentUrlService(documentUrlServiceMock.Object); + var result = apiDocumentUrlService.GetDocumentKeyByRoute("1234/some/where", null, false); + Assert.AreEqual(documentKey, result); + } + + [Test] + public void Can_Resolve_Document_Key_Without_Start_Node() + { + var documentKey = Guid.NewGuid(); + var documentUrlServiceMock = new Mock(); + documentUrlServiceMock + .Setup(m => m.GetDocumentKeyByRoute( + "/some/where", + It.IsAny(), + null, + false)) + .Returns(documentKey); + + var apiDocumentUrlService = new ApiDocumentUrlService(documentUrlServiceMock.Object); + var result = apiDocumentUrlService.GetDocumentKeyByRoute("/some/where", null, false); + Assert.AreEqual(documentKey, result); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs index 1480210534..f9d4f54659 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedContentCacheTests.cs @@ -6,7 +6,6 @@ using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.HybridCache; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -17,8 +16,9 @@ public class PublishedContentCacheTests : DeliveryApiTests private readonly Guid _contentTwoId = Guid.Parse("4EF11E1E-FB50-4627-8A86-E10ED6F4DCE4"); + private readonly Guid _contentThreeId = Guid.Parse("013387EE-57AF-4ABD-B03C-F991B0722CCA"); + private IPublishedContentCache _contentCache; - private IPublishedContentCache _contentCacheMock; private IDocumentUrlService _documentUrlService; [SetUp] @@ -34,37 +34,41 @@ public class PublishedContentCacheTests : DeliveryApiTests var contentTwoMock = new Mock(); ConfigurePublishedContentMock(contentTwoMock, _contentTwoId, "Content Two", "content-two", contentTypeTwoMock.Object, Array.Empty()); + var contentTypeThreeMock = new Mock(); + contentTypeThreeMock.SetupGet(m => m.Alias).Returns("theThirdContentType"); + var contentThreeMock = new Mock(); + ConfigurePublishedContentMock(contentThreeMock, _contentThreeId, "Content Three", "content-three", contentTypeThreeMock.Object, Array.Empty()); + var documentUrlService = new Mock(); documentUrlService - .Setup(x => x.GetDocumentKeyByRoute("content-one", It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.GetDocumentKeyByRoute("/content-one", It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_contentOneId); documentUrlService - .Setup(x => x.GetDocumentKeyByRoute("content-two", It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.GetDocumentKeyByRoute("/content-two", It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_contentTwoId); + documentUrlService + .Setup(x => x.GetDocumentKeyByRoute("/content-three", It.IsAny(), 1234, It.IsAny())) + .Returns(_contentThreeId); var contentCacheMock = new Mock(); - contentCacheMock - .Setup(m => m.GetByRoute(It.IsAny(), "content-one", null, null)) - .Returns(contentOneMock.Object); contentCacheMock .Setup(m => m.GetById(It.IsAny(), _contentOneId)) .Returns(contentOneMock.Object); - contentCacheMock - .Setup(m => m.GetByRoute(It.IsAny(), "content-two", null, null)) - .Returns(contentTwoMock.Object); contentCacheMock .Setup(m => m.GetById(It.IsAny(), _contentTwoId)) .Returns(contentTwoMock.Object); + contentCacheMock + .Setup(m => m.GetById(It.IsAny(), _contentThreeId)) + .Returns(contentThreeMock.Object); _contentCache = contentCacheMock.Object; - _contentCacheMock = contentCacheMock.Object; _documentUrlService = documentUrlService.Object; } [Test] public void PublishedContentCache_CanGetById() { - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), _documentUrlService, _contentCacheMock); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), CreateApiDocumentUrlService(), _contentCache); var content = publishedContentCache.GetById(_contentOneId); Assert.IsNotNull(content); Assert.AreEqual(_contentOneId, content.Key); @@ -75,18 +79,29 @@ public class PublishedContentCacheTests : DeliveryApiTests [Test] public void PublishedContentCache_CanGetByRoute() { - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), _documentUrlService, _contentCacheMock); - var content = publishedContentCache.GetByRoute("content-two"); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), CreateApiDocumentUrlService(), _contentCache); + var content = publishedContentCache.GetByRoute("/content-two"); Assert.IsNotNull(content); Assert.AreEqual(_contentTwoId, content.Key); Assert.AreEqual("content-two", content.UrlSegment); Assert.AreEqual("theOtherContentType", content.ContentType.Alias); } + [Test] + public void PublishedContentCache_CanGetByRoute_WithStartNodeIdPrefix() + { + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), CreateApiDocumentUrlService(), _contentCache); + var content = publishedContentCache.GetByRoute("1234/content-three"); + Assert.IsNotNull(content); + Assert.AreEqual(_contentThreeId, content.Key); + Assert.AreEqual("content-three", content.UrlSegment); + Assert.AreEqual("theThirdContentType", content.ContentType.Alias); + } + [Test] public void PublishedContentCache_CanGetByIds() { - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), _documentUrlService, _contentCacheMock); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(), CreateApiDocumentUrlService(), _contentCache); var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray(); Assert.AreEqual(2, content.Length); Assert.AreEqual(_contentOneId, content.First().Key); @@ -98,7 +113,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetById_SupportsDenyList(bool denied) { var denyList = denied ? new[] { "theOtherContentType" } : null; - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), CreateApiDocumentUrlService(), _contentCache); var content = publishedContentCache.GetById(_contentTwoId); if (denied) @@ -116,8 +131,8 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetByRoute_SupportsDenyList(bool denied) { var denyList = denied ? new[] { "theContentType" } : null; - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); - var content = publishedContentCache.GetByRoute("content-one"); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), CreateApiDocumentUrlService(), _contentCache); + var content = publishedContentCache.GetByRoute("/content-one"); if (denied) { @@ -134,7 +149,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetByIds_SupportsDenyList(string deniedContentType) { var denyList = new[] { deniedContentType }; - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), CreateApiDocumentUrlService(), _contentCache); var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray(); Assert.AreEqual(1, content.Length); @@ -152,7 +167,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetById_CanRetrieveContentTypesOutsideTheDenyList() { var denyList = new[] { "theContentType" }; - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), CreateApiDocumentUrlService(), _contentCache); var content = publishedContentCache.GetById(_contentTwoId); Assert.IsNotNull(content); Assert.AreEqual(_contentTwoId, content.Key); @@ -164,8 +179,8 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetByRoute_CanRetrieveContentTypesOutsideTheDenyList() { var denyList = new[] { "theOtherContentType" }; - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); - var content = publishedContentCache.GetByRoute("content-one"); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), CreateApiDocumentUrlService(), _contentCache); + var content = publishedContentCache.GetByRoute("/content-one"); Assert.IsNotNull(content); Assert.AreEqual(_contentOneId, content.Key); Assert.AreEqual("content-one", content.UrlSegment); @@ -176,7 +191,7 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_GetByIds_CanDenyAllRequestedContent() { var denyList = new[] { "theContentType", "theOtherContentType" }; - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), CreateApiDocumentUrlService(), _contentCache); var content = publishedContentCache.GetByIds(new[] { _contentOneId, _contentTwoId }).ToArray(); Assert.IsEmpty(content); } @@ -185,8 +200,8 @@ public class PublishedContentCacheTests : DeliveryApiTests public void PublishedContentCache_DenyListIsCaseInsensitive() { var denyList = new[] { "THEcontentTYPE" }; - var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), _documentUrlService, _contentCacheMock); - var content = publishedContentCache.GetByRoute("content-one"); + var publishedContentCache = new ApiPublishedContentCache(CreateRequestPreviewService(), CreateRequestCultureService(), CreateDeliveryApiSettings(denyList), CreateApiDocumentUrlService(), _contentCache); + var content = publishedContentCache.GetByRoute("/content-one"); Assert.IsNull(content); } @@ -211,4 +226,6 @@ public class PublishedContentCacheTests : DeliveryApiTests deliveryApiOptionsMonitorMock.SetupGet(s => s.CurrentValue).Returns(deliveryApiSettings); return deliveryApiOptionsMonitorMock.Object; } + + private IApiDocumentUrlService CreateApiDocumentUrlService() => new ApiDocumentUrlService(_documentUrlService); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index a427046135..928e337e53 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -357,6 +357,48 @@ public class RichTextParserTests : PropertyValueConverterTests Assert.IsEmpty(blockLevelBlock.Elements); } + [Test] + public void ParseElement_CanHandleWhitespaceAroundInlineElemements() + { + var parser = CreateRichTextElementParser(); + + var element = parser.Parse("

What follows from here is just a bunch of text.

") as RichTextRootElement; + Assert.IsNotNull(element); + var paragraphElement = element.Elements.Single() as RichTextGenericElement; + Assert.IsNotNull(paragraphElement); + + var childElements = paragraphElement.Elements.ToArray(); + Assert.AreEqual(7, childElements.Length); + + var childElementCounter = 0; + + void AssertNextChildElementIsText(string expectedText) + { + var textElement = childElements[childElementCounter++] as RichTextTextElement; + Assert.IsNotNull(textElement); + Assert.AreEqual(expectedText, textElement.Text); + } + + void AssertNextChildElementIsGeneric(string expectedTag, string expectedInnerText) + { + var genericElement = childElements[childElementCounter++] as RichTextGenericElement; + Assert.IsNotNull(genericElement); + Assert.AreEqual(expectedTag, genericElement.Tag); + Assert.AreEqual(1, genericElement.Elements.Count()); + var textElement = genericElement.Elements.First() as RichTextTextElement; + Assert.IsNotNull(textElement); + Assert.AreEqual(expectedInnerText, textElement.Text); + } + + AssertNextChildElementIsText("What follows from "); + AssertNextChildElementIsGeneric("strong", "here"); + AssertNextChildElementIsText(" "); + AssertNextChildElementIsGeneric("em", "is"); + AssertNextChildElementIsText(" "); + AssertNextChildElementIsGeneric("a", "just"); + AssertNextChildElementIsText(" a bunch of text."); + } + [Test] public void ParseMarkup_CanParseContentLink() { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/AncestorsSelectorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/AncestorsSelectorTests.cs new file mode 100644 index 0000000000..19d1359308 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/AncestorsSelectorTests.cs @@ -0,0 +1,75 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Api.Delivery.Querying.Selectors; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi.Selectors; + +[TestFixture] +public class AncestorsSelectorTests +{ + private readonly Guid _documentKey = Guid.NewGuid(); + private IDocumentNavigationQueryService _documentNavigationQueryService; + + [SetUp] + public void SetUp() + { + IEnumerable ancestorKeys = + [ + new("863e10d5-b0f8-421d-902d-5e4d1bd8e780"), + new("11fc9bdc-8366-4a6b-a9c2-6b8b2717c4b8") + ]; + var documentNavigationQueryServiceMock = new Mock(); + documentNavigationQueryServiceMock + .Setup(m => m.TryGetAncestorsKeys(_documentKey, out ancestorKeys)) + .Returns(true); + _documentNavigationQueryService = documentNavigationQueryServiceMock.Object; + } + + [TestCase(null)] + [TestCase(1234)] + public void Can_Build_Selector_Option_For_Path(int? documentStartNodeId) + { + var documentUrlServiceMock = new Mock(); + documentUrlServiceMock + .Setup(m => m.GetDocumentKeyByRoute( + "/some/where", + It.IsAny(), + documentStartNodeId, + false)) + .Returns(_documentKey); + + var requestRoutingServiceMock = new Mock(); + requestRoutingServiceMock.Setup(m => m.GetContentRoute("/some/where")).Returns($"{documentStartNodeId}/some/where"); + + var subject = new AncestorsSelector( + requestRoutingServiceMock.Object, + Mock.Of(), + Mock.Of(), + new ApiDocumentUrlService(documentUrlServiceMock.Object), + _documentNavigationQueryService); + + var result = subject.BuildSelectorOption("ancestors:/some/where"); + Assert.AreEqual(2, result.Values.Length); + Assert.AreEqual("863e10d5-b0f8-421d-902d-5e4d1bd8e780", result.Values[0]); + Assert.AreEqual("11fc9bdc-8366-4a6b-a9c2-6b8b2717c4b8", result.Values[1]); + } + + [Test] + public void Can_Build_Selector_Option_For_Id() + { + var subject = new AncestorsSelector( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + _documentNavigationQueryService); + + var result = subject.BuildSelectorOption($"ancestors:{_documentKey:D}"); + Assert.AreEqual(2, result.Values.Length); + Assert.AreEqual("863e10d5-b0f8-421d-902d-5e4d1bd8e780", result.Values[0]); + Assert.AreEqual("11fc9bdc-8366-4a6b-a9c2-6b8b2717c4b8", result.Values[1]); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/ChildrenSelectorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/ChildrenSelectorTests.cs new file mode 100644 index 0000000000..33a3912de0 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/ChildrenSelectorTests.cs @@ -0,0 +1,56 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Api.Delivery.Querying.Selectors; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi.Selectors; + +[TestFixture] +public class ChildrenSelectorTests +{ + [TestCase(null)] + [TestCase(1234)] + public void Can_Build_Selector_Option_For_Path(int? documentStartNodeId) + { + var documentKey = Guid.NewGuid(); + + var documentUrlServiceMock = new Mock(); + documentUrlServiceMock + .Setup(m => m.GetDocumentKeyByRoute( + "/some/where", + It.IsAny(), + documentStartNodeId, + false)) + .Returns(documentKey); + + var requestRoutingServiceMock = new Mock(); + requestRoutingServiceMock.Setup(m => m.GetContentRoute("/some/where")).Returns($"{documentStartNodeId}/some/where"); + + var subject = new ChildrenSelector( + requestRoutingServiceMock.Object, + Mock.Of(), + Mock.Of(), + new ApiDocumentUrlService(documentUrlServiceMock.Object)); + + var result = subject.BuildSelectorOption("children:/some/where"); + Assert.AreEqual(1, result.Values.Length); + Assert.AreEqual(documentKey.ToString("D"), result.Values[0]); + } + + [Test] + public void Can_Build_Selector_Option_For_Id() + { + var documentKey = Guid.NewGuid(); + + var subject = new ChildrenSelector( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + + var result = subject.BuildSelectorOption($"children:{documentKey:D}"); + Assert.AreEqual(1, result.Values.Length); + Assert.AreEqual(documentKey.ToString("D"), result.Values[0]); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/DescendantsSelectorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/DescendantsSelectorTests.cs new file mode 100644 index 0000000000..d322dc898a --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/Selectors/DescendantsSelectorTests.cs @@ -0,0 +1,56 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Api.Delivery.Querying.Selectors; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi.Selectors; + +[TestFixture] +public class DescendantsSelectorTests +{ + [TestCase(null)] + [TestCase(1234)] + public void Can_Build_Selector_Option_For_Path(int? documentStartNodeId) + { + var documentKey = Guid.NewGuid(); + + var documentUrlServiceMock = new Mock(); + documentUrlServiceMock + .Setup(m => m.GetDocumentKeyByRoute( + "/some/where", + It.IsAny(), + documentStartNodeId, + false)) + .Returns(documentKey); + + var requestRoutingServiceMock = new Mock(); + requestRoutingServiceMock.Setup(m => m.GetContentRoute("/some/where")).Returns($"{documentStartNodeId}/some/where"); + + var subject = new DescendantsSelector( + requestRoutingServiceMock.Object, + Mock.Of(), + Mock.Of(), + new ApiDocumentUrlService(documentUrlServiceMock.Object)); + + var result = subject.BuildSelectorOption("descendants:/some/where"); + Assert.AreEqual(1, result.Values.Length); + Assert.AreEqual(documentKey.ToString("D"), result.Values[0]); + } + + [Test] + public void Can_Build_Selector_Option_For_Id() + { + var documentKey = Guid.NewGuid(); + + var subject = new DescendantsSelector( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + + var result = subject.BuildSelectorOption($"descendants:{documentKey:D}"); + Assert.AreEqual(1, result.Values.Length); + Assert.AreEqual(documentKey.ToString("D"), result.Values[0]); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs index a4ef26ece9..461edeef69 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -9,7 +9,6 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Serialization; -using Umbraco.Cms.Tests.Common.Builders; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; @@ -47,54 +46,30 @@ public class PropertyEditorValueConverterTests } } - [TestCase("TRUE", null, true)] - [TestCase("True", null, true)] - [TestCase("true", null, true)] - [TestCase("1", null, true)] - [TestCase(1, null, true)] - [TestCase(true, null, true)] - [TestCase("FALSE", null, false)] - [TestCase("False", null, false)] - [TestCase("false", null, false)] - [TestCase("0", null, false)] - [TestCase(0, null, false)] - [TestCase(false, null, false)] - [TestCase("", null, false)] - [TestCase("blah", null, false)] - [TestCase(null, false, false)] - [TestCase(null, true, true)] - public void CanConvertTrueFalsePropertyEditor(object value, bool initialStateConfigurationValue, bool expected) + [TestCase("TRUE", true)] + [TestCase("True", true)] + [TestCase("true", true)] + [TestCase("1", true)] + [TestCase(1, true)] + [TestCase(true, true)] + [TestCase("FALSE", false)] + [TestCase("False", false)] + [TestCase("false", false)] + [TestCase("0", false)] + [TestCase(0, false)] + [TestCase(false, false)] + [TestCase("", false)] + [TestCase(null, false)] + [TestCase("blah", false)] + public void CanConvertYesNoPropertyEditor(object value, bool expected) { - var publishedDataType = CreatePublishedDataType(initialStateConfigurationValue); - - var publishedPropertyTypeMock = new Mock(); - publishedPropertyTypeMock - .SetupGet(p => p.DataType) - .Returns(publishedDataType); - var converter = new YesNoValueConverter(); - var intermediateResult = converter.ConvertSourceToIntermediate(null, publishedPropertyTypeMock.Object, value, false); - var result = converter.ConvertIntermediateToObject(null, publishedPropertyTypeMock.Object, PropertyCacheLevel.Element, intermediateResult, false); + var result = + converter.ConvertSourceToIntermediate(null, null, value, false); // does not use type for conversion Assert.AreEqual(expected, result); } - private static PublishedDataType CreatePublishedDataType(bool initialStateConfigurationValue) - { - var dataTypeConfiguration = new TrueFalseConfiguration - { - InitialState = initialStateConfigurationValue - }; - - var dateTypeMock = new Mock(); - dateTypeMock.SetupGet(x => x.Id).Returns(1000); - dateTypeMock.SetupGet(x => x.EditorAlias).Returns(global::Umbraco.Cms.Core.Constants.PropertyEditors.Aliases.Boolean); - dateTypeMock.SetupGet(x => x.EditorUiAlias).Returns("Umb.PropertyEditorUi.Toggle"); - dateTypeMock.SetupGet(x => x.ConfigurationObject).Returns(dataTypeConfiguration); - - return new PublishedDataType(dateTypeMock.Object.Id, dateTypeMock.Object.EditorAlias, dateTypeMock.Object.EditorUiAlias, new Lazy(() => dataTypeConfiguration)); - } - [TestCase("[\"apples\"]", new[] { "apples" })] [TestCase("[\"apples\",\"oranges\"]", new[] { "apples", "oranges" })] [TestCase("[\"apples\",\"oranges\",\"pears\"]", new[] { "apples", "oranges", "pears" })] diff --git a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs index 7f02686ae6..f6d2be89a9 100644 --- a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs +++ b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs @@ -73,13 +73,25 @@ internal class UmbracoCmsSchema public required HelpPageSettings HelpPage { get; set; } - public required InstallDefaultDataSettings DefaultDataCreation { get; set; } + public required InstallDefaultDataNamedOptions InstallDefaultData { get; set; } public required DataTypesSettings DataTypes { get; set; } public required MarketplaceSettings Marketplace { get; set; } public required WebhookSettings Webhook { get; set; } + public required CacheSettings Cache { get; set; } } + + public class InstallDefaultDataNamedOptions + { + public required InstallDefaultDataSettings Languages { get; set; } + + public required InstallDefaultDataSettings DataTypes { get; set; } + + public required InstallDefaultDataSettings MediaTypes { get; set; } + + public required InstallDefaultDataSettings MemberTypes { get; set; } + } } diff --git a/version.json b/version.json index 079f99e21d..dded7533aa 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "15.2.0-rc", + "version": "15.3.0-rc", "assemblyVersion": { "precision": "build" },