diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 9a0703cd98..aed4d68cf2 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -137,49 +137,54 @@ stages: umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ] jobs: # C# API Reference -# - job: -# displayName: Build C# API Reference -# pool: -# vmImage: 'windows-latest' -# steps: -# - task: PowerShell@2 -# displayName: Install DocFX -# inputs: -# targetType: inline -# script: | -# choco install docfx --version=2.59.4 -y -# if ($lastexitcode -ne 0){ -# throw ("Error installing DocFX") -# } -# - task: PowerShell@2 -# displayName: Generate metadata -# inputs: -# targetType: inline -# script: | -# docfx metadata "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json" -# if ($lastexitcode -ne 0){ -# throw ("Error generating metadata.") -# } -# - task: PowerShell@2 -# displayName: Generate documentation -# inputs: -# targetType: inline -# script: | -# docfx build "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json" -# if ($lastexitcode -ne 0){ -# throw ("Error generating documentation.") -# } -# - task: ArchiveFiles@2 -# displayName: Archive C# Docs -# inputs: -# rootFolderOrFile: $(Build.SourcesDirectory)/build/csharp-docs/_site -# includeRootFolder: false -# archiveFile: $(Build.ArtifactStagingDirectory)/csharp-docs.zip -# - task: PublishPipelineArtifact@1 -# displayName: Publish C# Docs -# inputs: -# targetPath: $(Build.ArtifactStagingDirectory)/csharp-docs.zip -# artifact: csharp-docs + - job: + displayName: Build C# API Reference + pool: + vmImage: 'windows-latest' + steps: + - task: UseDotNet@2 + displayName: Use .NET $(dotnetVersion) + inputs: + version: $(dotnetVersion) + includePreviewVersions: $(dotnetIncludePreviewVersions) + - task: PowerShell@2 + displayName: Install DocFX + inputs: + targetType: inline + script: | + dotnet tool install -g docfx --version 2.72.1 + if ($lastexitcode -ne 0){ + throw ("Error installing DocFX") + } + - task: PowerShell@2 + displayName: Generate metadata + inputs: + targetType: inline + script: | + docfx metadata "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json" + if ($lastexitcode -ne 0){ + throw ("Error generating metadata.") + } + - task: PowerShell@2 + displayName: Generate documentation + inputs: + targetType: inline + script: | + docfx build "$(Build.SourcesDirectory)/build/csharp-docs/docfx.json" + if ($lastexitcode -ne 0){ + throw ("Error generating documentation.") + } + - task: ArchiveFiles@2 + displayName: Archive C# Docs + inputs: + rootFolderOrFile: $(Build.SourcesDirectory)/build/csharp-docs/_site + includeRootFolder: false + archiveFile: $(Build.ArtifactStagingDirectory)/csharp-docs.zip + - task: PublishPipelineArtifact@1 + displayName: Publish C# Docs + inputs: + targetPath: $(Build.ArtifactStagingDirectory)/csharp-docs.zip + artifact: csharp-docs # js API Reference - job: diff --git a/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs b/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs index 8ffcd00d67..10f052485a 100644 --- a/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs +++ b/src/Umbraco.Cms.Api.Delivery/Json/DeliveryApiJsonTypeResolver.cs @@ -22,7 +22,7 @@ public class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver } else if (jsonTypeInfo.Type == typeof(IRichTextElement)) { - ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(RichTextGenericElement), typeof(RichTextTextElement)); + ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(RichTextRootElement), typeof(RichTextGenericElement), typeof(RichTextTextElement)); } return jsonTypeInfo; diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs index 7359b122e1..0d140feef3 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -470,4 +470,7 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase public bool IsUnique { get; set; } } + + public override string Length => "length"; + public override string Substring => "substr"; } diff --git a/src/Umbraco.Core/Blocks/IPartialViewBlockEngine.cs b/src/Umbraco.Core/Blocks/IPartialViewBlockEngine.cs new file mode 100644 index 0000000000..3b462d0865 --- /dev/null +++ b/src/Umbraco.Core/Blocks/IPartialViewBlockEngine.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.Blocks; + +public interface IPartialViewBlockEngine +{ + Task ExecuteAsync(IBlockReference blockReference); +} diff --git a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs index 006c590163..fce3b36373 100644 --- a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs @@ -82,6 +82,10 @@ public class RichTextEditorSettings { Alias = "umbmediapicker", Name = "Image", Mode = RichTextEditorCommandMode.Insert, }, + new RichTextEditorCommand + { + Alias = "umbblockpicker", Name = "Block", Mode = RichTextEditorCommandMode.All, + }, new RichTextEditorCommand { Alias = "umbmacro", Name = "Macro", Mode = RichTextEditorCommandMode.All }, new RichTextEditorCommand { Alias = "table", Name = "Table", Mode = RichTextEditorCommandMode.Insert }, new RichTextEditorCommand diff --git a/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs b/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs new file mode 100644 index 0000000000..b772a103ba --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/WebhookSettings.cs @@ -0,0 +1,34 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions(Constants.Configuration.ConfigWebhook)] +public class WebhookSettings +{ + private const bool StaticEnabled = true; + private const int StaticMaximumRetries = 5; + + /// + /// Gets or sets a value indicating whether webhooks are enabled. + /// + /// + /// + /// By default, webhooks are enabled. + /// If this option is set to false webhooks will no longer send web-requests. + /// + /// + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; + + /// + /// Gets or sets a value indicating the maximum number of retries for all webhooks. + /// + /// + /// + /// By default, maximum number of retries is 5. + /// If this option is set to 0 webhooks will no longer retry. + /// + /// + [DefaultValue(StaticMaximumRetries)] + public int MaximumRetries { get; set; } = StaticMaximumRetries; +} diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index dc36715585..aa1f19c791 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -147,6 +147,8 @@ public static partial class Constants public const string LogViewer = "logViewer"; + public const string Webhooks = "webhooks"; + public static class Groups { public const string Settings = "settingsGroup"; diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index eca35dbad0..62703dfc6a 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -65,6 +65,7 @@ public static partial class Constants public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; public const string ConfigPackageManifests = ConfigPrefix + "PackageManifests"; + public const string ConfigWebhook = ConfigPrefix + "Webhook"; public static class NamedOptions { diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 5cfc2808fc..5aaeb2ba61 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -158,5 +158,10 @@ public static partial class Constants /// System user group icon /// public const string UserGroup = "icon-users"; + + /// + /// Webhooks icon + /// + public const string Webhooks = "icon-directions-alt"; } } diff --git a/src/Umbraco.Core/Constants-WebhookEvents.cs b/src/Umbraco.Core/Constants-WebhookEvents.cs new file mode 100644 index 0000000000..24fe890221 --- /dev/null +++ b/src/Umbraco.Core/Constants-WebhookEvents.cs @@ -0,0 +1,32 @@ +namespace Umbraco.Cms.Core; + +public static partial class Constants +{ + public static class WebhookEvents + { + /// + /// Webhook event name for content publish. + /// + public const string ContentPublish = "ContentPublish"; + + /// + /// Webhook event name for content delete. + /// + public const string ContentDelete = "ContentDelete"; + + /// + /// Webhook event name for content unpublish. + /// + public const string ContentUnpublish = "ContentUnpublish"; + + /// + /// Webhook event name for media delete. + /// + public const string MediaDelete = "MediaDelete"; + + /// + /// Webhook event name for media save. + /// + public const string MediaSave = "MediaSave"; + } +} diff --git a/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs b/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs index 067cdf068d..50b9b5d581 100644 --- a/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs +++ b/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs @@ -1,8 +1,13 @@ -using Umbraco.Cms.Core.Models.DeliveryApi; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.DeliveryApi; namespace Umbraco.Cms.Core.DeliveryApi; public interface IApiRichTextElementParser { + // NOTE: remember to also remove the default implementation of the method overload when this one is removed. + [Obsolete($"Please use the overload that accepts {nameof(RichTextBlockModel)}. Will be removed in V15.")] IRichTextElement? Parse(string html); + + IRichTextElement? Parse(string html, RichTextBlockModel? richTextBlockModel) => null; } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 06fe1b8153..74ad5a7dc3 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -15,10 +15,13 @@ using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Sections; using Umbraco.Cms.Core.Snippets; +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; +using Umbraco.Cms.Core.DynamicRoot.Origin; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Tour; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.WebAssets; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection; @@ -77,6 +80,20 @@ public static partial class UmbracoBuilderExtensions .Append() .Append() .Append(); + + builder.DynamicRootOriginFinders() + .Append() + .Append() + .Append() + .Append() + .Append(); + + builder.DynamicRootSteps() + .Append() + .Append() + .Append() + .Append(); + builder.Components(); // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards builder.Dashboards() @@ -128,6 +145,7 @@ public static partial class UmbracoBuilderExtensions builder.FilterHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.SortHandlers().Add(() => builder.TypeLoader.GetTypes()); builder.ContentIndexHandlers().Add(() => builder.TypeLoader.GetTypes()); + builder.WebhookEvents().AddCoreWebhooks(); } /// @@ -195,6 +213,18 @@ public static partial class UmbracoBuilderExtensions public static SectionCollectionBuilder Sections(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + public static DynamicRootOriginFinderCollectionBuilder DynamicRootOriginFinders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + public static DynamicRootQueryStepCollectionBuilder DynamicRootSteps(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the backoffice sections/applications collection builder. + /// + /// The builder. + public static WebhookEventCollectionBuilder WebhookEvents(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + /// /// Gets the components collection builder. /// diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index e90953645c..ceff2cf7ce 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -88,7 +88,8 @@ public static partial class UmbracoBuilderExtensions .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); // Configure connection string and ensure it's updated when the configuration changes builder.Services.AddSingleton, ConfigureConnectionStrings>(); diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index cfc447f0fa..2bf71d22b3 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -37,10 +37,12 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Snippets; +using Umbraco.Cms.Core.DynamicRoot; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection @@ -358,6 +360,10 @@ namespace Umbraco.Cms.Core.DependencyInjection // Register filestream security analyzers Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/Deploy/IFileSource.cs b/src/Umbraco.Core/Deploy/IFileSource.cs index ed169b9df5..e56f9a715e 100644 --- a/src/Umbraco.Core/Deploy/IFileSource.cs +++ b/src/Umbraco.Core/Deploy/IFileSource.cs @@ -68,18 +68,24 @@ public interface IFileSource /// A collection of file types which can store the files. void GetFiles(IEnumerable udis, IFileTypeCollection fileTypes); + // TODO (V14): Remove obsolete method and default implementation for GetFilesAsync overloads. + /// /// Gets files and store them using a file store. /// /// The udis of the files to get. /// A collection of file types which can store the files. /// A cancellation token. + [Obsolete("Please use the method overload taking all parameters. This method overload will be removed in Umbraco 14.")] Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, CancellationToken token); - ///// - ///// Gets the content of a file as a bytes array. - ///// - ///// A file entity identifier. - ///// A byte array containing the file content. - // byte[] GetFileBytes(StringUdi Udi); + /// + /// Gets files and store them using a file store. + /// + /// The udis of the files to get. + /// A collection of file types which can store the files. + /// A flag indicating whether to continue if a file isn't found or to stop and throw a FileNotFoundException. + /// A cancellation token. + Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, bool continueOnFileNotFound, CancellationToken token) + => GetFilesAsync(udis, fileTypes, token); } diff --git a/src/Umbraco.Core/DynamicRoot/DynamicRootContext.cs b/src/Umbraco.Core/DynamicRoot/DynamicRootContext.cs new file mode 100644 index 0000000000..755c8a3c71 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/DynamicRootContext.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.DynamicRoot; + +public struct DynamicRootContext +{ + public required Guid? CurrentKey { get; set; } + + public required Guid ParentKey { get; set; } +} diff --git a/src/Umbraco.Core/DynamicRoot/DynamicRootNodeQuery.cs b/src/Umbraco.Core/DynamicRoot/DynamicRootNodeQuery.cs new file mode 100644 index 0000000000..08371e8dae --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/DynamicRootNodeQuery.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +namespace Umbraco.Cms.Core.DynamicRoot; + +/// +/// Specifies origin and context data with optional query steps to find Dynamic Roots +/// +public class DynamicRootNodeQuery +{ + public required string OriginAlias { get; set; } + + public Guid? OriginKey { get; set; } + + public required DynamicRootContext Context { get; set; } + + public IEnumerable QuerySteps { get; set; } = Array.Empty(); +} diff --git a/src/Umbraco.Core/DynamicRoot/DynamicRootService.cs b/src/Umbraco.Core/DynamicRoot/DynamicRootService.cs new file mode 100644 index 0000000000..23b8f75878 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/DynamicRootService.cs @@ -0,0 +1,74 @@ +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; +using Umbraco.Cms.Core.DynamicRoot.Origin; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.DynamicRoot; + +public class DynamicRootService : IDynamicRootService +{ + private readonly DynamicRootOriginFinderCollection _originFinderCollection; + private readonly DynamicRootQueryStepCollection _queryStepCollection; + + public DynamicRootService(DynamicRootOriginFinderCollection originFinderCollection, DynamicRootQueryStepCollection queryStepCollection) + { + _originFinderCollection = originFinderCollection; + _queryStepCollection = queryStepCollection; + } + + public async Task> GetDynamicRootsAsync(DynamicRootNodeQuery dynamicRootNodeQuery) + { + var originKey = FindOriginKey(dynamicRootNodeQuery); + + if (originKey is null) + { + return Array.Empty(); + } + + // no steps means the origin is the root + if (dynamicRootNodeQuery.QuerySteps.Any() is false) + { + return originKey.Value.Yield(); + } + + // start with the origin + ICollection filtered = new []{originKey.Value}; + + // resolved each Query Step using the result of the previous step (or origin) + foreach (DynamicRootQueryStep startNodeSelectorFilter in dynamicRootNodeQuery.QuerySteps) + { + filtered = await ExcuteFiltersAsync(filtered, startNodeSelectorFilter); + } + + return filtered; + } + + internal async Task> ExcuteFiltersAsync(ICollection origin, DynamicRootQueryStep dynamicRootQueryStep) + { + foreach (IDynamicRootQueryStep queryStep in _queryStepCollection) + { + var queryStepAttempt = await queryStep.ExecuteAsync(origin, dynamicRootQueryStep); + if (queryStepAttempt is { Success: true, Result: not null }) + { + return queryStepAttempt.Result; + } + } + + throw new NotSupportedException($"Did not find any filteres that could handle {dynamicRootQueryStep.Alias}"); + } + + internal Guid? FindOriginKey(DynamicRootNodeQuery dynamicRootNodeQuery) + { + foreach (IDynamicRootOriginFinder originFinder in _originFinderCollection) + { + Guid? originKey = originFinder.FindOriginKey(dynamicRootNodeQuery); + + if (originKey is not null) + { + return originKey; + } + } + + return null; + } +} + diff --git a/src/Umbraco.Core/DynamicRoot/IDynamicRootService.cs b/src/Umbraco.Core/DynamicRoot/IDynamicRootService.cs new file mode 100644 index 0000000000..226b5d9f82 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/IDynamicRootService.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.DynamicRoot; + +/// +/// Supports finding content roots for pickers (like MNTP) in a dynamic fashion +/// +public interface IDynamicRootService +{ + Task> GetDynamicRootsAsync(DynamicRootNodeQuery dynamicRootNodeQuery); +} diff --git a/src/Umbraco.Core/DynamicRoot/Origin/ByKeyDynamicRootOriginFinder.cs b/src/Umbraco.Core/DynamicRoot/Origin/ByKeyDynamicRootOriginFinder.cs new file mode 100644 index 0000000000..f720d08e1b --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/Origin/ByKeyDynamicRootOriginFinder.cs @@ -0,0 +1,38 @@ +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.DynamicRoot.Origin; + +public class ByKeyDynamicRootOriginFinder : IDynamicRootOriginFinder +{ + protected virtual string SupportedOriginType { get; set; } = "ByKey"; + + private readonly IEntityService _entityService; + + private ISet _allowedObjectTypes = new HashSet(new[] + { + Constants.ObjectTypes.Document, Constants.ObjectTypes.SystemRoot + }); + + public ByKeyDynamicRootOriginFinder(IEntityService entityService) + { + _entityService = entityService; + } + + public virtual Guid? FindOriginKey(DynamicRootNodeQuery query) + { + if (query.OriginAlias != SupportedOriginType || query.OriginKey is null) + { + return null; + } + + IEntitySlim? entity = _entityService.Get(query.OriginKey.Value); + + if (entity is null || _allowedObjectTypes.Contains(entity.NodeObjectType) is false) + { + return null; + } + + return entity.Key; + } +} diff --git a/src/Umbraco.Core/DynamicRoot/Origin/CurrentDynamicRootOriginFinder.cs b/src/Umbraco.Core/DynamicRoot/Origin/CurrentDynamicRootOriginFinder.cs new file mode 100644 index 0000000000..cb4bd6d26e --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/Origin/CurrentDynamicRootOriginFinder.cs @@ -0,0 +1,21 @@ +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.DynamicRoot.Origin; + +public class CurrentDynamicRootOriginFinder : ByKeyDynamicRootOriginFinder +{ + public CurrentDynamicRootOriginFinder(IEntityService entityService) + : base(entityService) + { + } + + protected override string SupportedOriginType { get; set; } = "Current"; + + public override Guid? FindOriginKey(DynamicRootNodeQuery query) + { + query.OriginKey = query.Context.CurrentKey; + var baseResult = base.FindOriginKey(query); + + return baseResult; + } +} diff --git a/src/Umbraco.Core/DynamicRoot/Origin/DynamicRootOriginCollectionBuilder.cs b/src/Umbraco.Core/DynamicRoot/Origin/DynamicRootOriginCollectionBuilder.cs new file mode 100644 index 0000000000..1ee1693bb1 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/Origin/DynamicRootOriginCollectionBuilder.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.DynamicRoot.Origin; + +public class DynamicRootOriginFinderCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override DynamicRootOriginFinderCollectionBuilder This => this; +} diff --git a/src/Umbraco.Core/DynamicRoot/Origin/DynamicRootOriginFinderCollection.cs b/src/Umbraco.Core/DynamicRoot/Origin/DynamicRootOriginFinderCollection.cs new file mode 100644 index 0000000000..1d137ca924 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/Origin/DynamicRootOriginFinderCollection.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.DynamicRoot.Origin; + +public class DynamicRootOriginFinderCollection : BuilderCollectionBase +{ + public DynamicRootOriginFinderCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.Core/DynamicRoot/Origin/IDynamicRootOriginFinder.cs b/src/Umbraco.Core/DynamicRoot/Origin/IDynamicRootOriginFinder.cs new file mode 100644 index 0000000000..838f1822f7 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/Origin/IDynamicRootOriginFinder.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.DynamicRoot.Origin; + +/// +/// Supports finding the Origin For a given query +/// +public interface IDynamicRootOriginFinder +{ + Guid? FindOriginKey(DynamicRootNodeQuery dynamicRootNodeQuery); +} diff --git a/src/Umbraco.Core/DynamicRoot/Origin/ParentDynamicRootOriginFinder.cs b/src/Umbraco.Core/DynamicRoot/Origin/ParentDynamicRootOriginFinder.cs new file mode 100644 index 0000000000..3ab6f4e71f --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/Origin/ParentDynamicRootOriginFinder.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.DynamicRoot.Origin; + +public class ParentDynamicRootOriginFinder : ByKeyDynamicRootOriginFinder +{ + public ParentDynamicRootOriginFinder(IEntityService entityService) : base(entityService) + { + } + + protected override string SupportedOriginType { get; set; } = "Parent"; + + public override Guid? FindOriginKey(DynamicRootNodeQuery query) + { + query.OriginKey = query.Context.ParentKey; + var baseResult = base.FindOriginKey(query); + + return baseResult; + } +} diff --git a/src/Umbraco.Core/DynamicRoot/Origin/RootDynamicRootOriginFinder.cs b/src/Umbraco.Core/DynamicRoot/Origin/RootDynamicRootOriginFinder.cs new file mode 100644 index 0000000000..44766fb2dc --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/Origin/RootDynamicRootOriginFinder.cs @@ -0,0 +1,70 @@ +using System.Globalization; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.DynamicRoot.Origin; + +public class RootDynamicRootOriginFinder : IDynamicRootOriginFinder +{ + private readonly IEntityService _entityService; + + public RootDynamicRootOriginFinder(IEntityService entityService) + { + _entityService = entityService; + } + + private ISet _allowedObjectTypes = new HashSet(new[] + { + Constants.ObjectTypes.Document, Constants.ObjectTypes.SystemRoot + }); + + protected virtual string SupportedOriginType { get; set; } = "Root"; + + public virtual Guid? FindOriginKey(DynamicRootNodeQuery query) + { + if (query.OriginAlias != SupportedOriginType) + { + return null; + } + + var entity = _entityService.Get(query.Context.ParentKey); + + if (entity is null || _allowedObjectTypes.Contains(entity.NodeObjectType) is false) + { + return null; + } + + var path = entity.Path.Split(","); + if (path.Length < 2) + { + return null; + } + + + var rootId = GetRootId(path); + IEntitySlim? root = rootId is null ? null : _entityService.Get(rootId.Value); + + if (root is null + || root.NodeObjectType != Constants.ObjectTypes.Document) + { + return null; + } + + return root.Key; + } + + private static int? GetRootId(string[] path) + { + foreach (var contentId in path) + { + if (contentId is Constants.System.RootString or Constants.System.RecycleBinContentString) + { + continue; + } + + return int.Parse(contentId, NumberStyles.Integer, CultureInfo.InvariantCulture); + } + + return null; + } +} diff --git a/src/Umbraco.Core/DynamicRoot/Origin/SiteDynamicRootOriginFinder.cs b/src/Umbraco.Core/DynamicRoot/Origin/SiteDynamicRootOriginFinder.cs new file mode 100644 index 0000000000..d1e515de59 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/Origin/SiteDynamicRootOriginFinder.cs @@ -0,0 +1,55 @@ +using System.Globalization; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.DynamicRoot.Origin; + +public class SiteDynamicRootOriginFinder : RootDynamicRootOriginFinder +{ + private readonly IEntityService _entityService; + private readonly IDomainService _domainService; + + public SiteDynamicRootOriginFinder(IEntityService entityService, IDomainService domainService) : base(entityService) + { + _entityService = entityService; + _domainService = domainService; + } + + protected override string SupportedOriginType { get; set; } = "Site"; + + public override Guid? FindOriginKey(DynamicRootNodeQuery query) + { + if (query.OriginAlias != SupportedOriginType || query.Context.CurrentKey.HasValue is false) + { + return null; + } + + IEntitySlim? entity = _entityService.Get(query.Context.CurrentKey.Value); + if (entity is null || entity.NodeObjectType != Constants.ObjectTypes.Document) + { + return null; + } + + + IEnumerable reversePath = entity.Path.Split(",").Reverse(); + foreach (var contentIdString in reversePath) + { + var contentId = int.Parse(contentIdString, NumberStyles.Integer, CultureInfo.InvariantCulture); + IEnumerable domains = _domainService.GetAssignedDomains(contentId, true); + if (!domains.Any()) + { + continue; + } + + IEntitySlim? entityWithDomain = _entityService.Get(contentId); + if (entityWithDomain is not null) + { + return entityWithDomain.Key; + } + } + + // No domains assigned, we fall back to root. + return base.FindOriginKey(query); + } +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStep.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStep.cs new file mode 100644 index 0000000000..be4d7ae030 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStep.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public class DynamicRootQueryStep +{ + /// + /// Empty means all Doctypes + /// + public IEnumerable AnyOfDocTypeKeys { get; set; } = Array.Empty(); + + public string Alias { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStepCollection.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStepCollection.cs new file mode 100644 index 0000000000..ba084025da --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStepCollection.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public class DynamicRootQueryStepCollection : BuilderCollectionBase +{ + public DynamicRootQueryStepCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStepCollectionBuilder.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStepCollectionBuilder.cs new file mode 100644 index 0000000000..b10f4ea2e2 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStepCollectionBuilder.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public class DynamicRootQueryStepCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override DynamicRootQueryStepCollectionBuilder This => this; +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/FarthestAncestorOrSelfDynamicRootQueryStep.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/FarthestAncestorOrSelfDynamicRootQueryStep.cs new file mode 100644 index 0000000000..11303cf3d9 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/FarthestAncestorOrSelfDynamicRootQueryStep.cs @@ -0,0 +1,37 @@ +using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public class FarthestAncestorOrSelfDynamicRootQueryStep : IDynamicRootQueryStep +{ + private readonly ICoreScopeProvider _scopeProvider; + private readonly IDynamicRootRepository _nodeFilterRepository; + + public FarthestAncestorOrSelfDynamicRootQueryStep(ICoreScopeProvider scopeProvider, IDynamicRootRepository nodeFilterRepository) + { + _scopeProvider = scopeProvider; + _nodeFilterRepository = nodeFilterRepository; + } + + protected virtual string SupportedDirectionAlias { get; set; } = "FarthestAncestorOrSelf"; + + public async Task>> ExecuteAsync(ICollection origins, DynamicRootQueryStep filter) + { + if (filter.Alias != SupportedDirectionAlias) + { + return Attempt>.Fail(); + } + + if (origins.Count < 1) + { + return Attempt>.Succeed(Array.Empty()); + } + + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + var result = (await _nodeFilterRepository.FarthestAncestorOrSelfAsync(origins, filter))?.ToSingleItemCollection() ?? Array.Empty(); + + return Attempt>.Succeed(result); + } +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/FarthestDescendantOrSelfDynamicRootQueryStep.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/FarthestDescendantOrSelfDynamicRootQueryStep.cs new file mode 100644 index 0000000000..a67d1bdf73 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/FarthestDescendantOrSelfDynamicRootQueryStep.cs @@ -0,0 +1,35 @@ +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public class FarthestDescendantOrSelfDynamicRootQueryStep : IDynamicRootQueryStep +{ + private readonly ICoreScopeProvider _scopeProvider; + private readonly IDynamicRootRepository _nodeFilterRepository; + + public FarthestDescendantOrSelfDynamicRootQueryStep(ICoreScopeProvider scopeProvider, IDynamicRootRepository nodeFilterRepository) + { + _scopeProvider = scopeProvider; + _nodeFilterRepository = nodeFilterRepository; + } + + protected virtual string SupportedDirectionAlias { get; set; } = "FarthestDescendantOrSelf"; + + public async Task>> ExecuteAsync(ICollection origins, DynamicRootQueryStep filter) + { + if (filter.Alias != SupportedDirectionAlias) + { + return Attempt>.Fail(); + } + + if (origins.Count < 1) + { + return Attempt>.Succeed(Array.Empty()); + } + + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + var result = await _nodeFilterRepository.FarthestDescendantOrSelfAsync(origins, filter); + + return Attempt>.Succeed(result); + } +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/IDynamicRootQueryStep.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/IDynamicRootQueryStep.cs new file mode 100644 index 0000000000..a72b86474a --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/IDynamicRootQueryStep.cs @@ -0,0 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public interface IDynamicRootQueryStep +{ + Task>> ExecuteAsync(ICollection origins, DynamicRootQueryStep filter); +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/IDynamicRootRepository.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/IDynamicRootRepository.cs new file mode 100644 index 0000000000..10a35557a4 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/IDynamicRootRepository.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public interface IDynamicRootRepository +{ + Task NearestAncestorOrSelfAsync(IEnumerable origins, DynamicRootQueryStep queryStep); + + Task FarthestAncestorOrSelfAsync(IEnumerable origins, DynamicRootQueryStep queryStep); + + Task> NearestDescendantOrSelfAsync(ICollection origins, DynamicRootQueryStep queryStep); + + Task> FarthestDescendantOrSelfAsync(ICollection origins, DynamicRootQueryStep queryStep); +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/NearestAncestorOrSelfDynamicRootQueryStep.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/NearestAncestorOrSelfDynamicRootQueryStep.cs new file mode 100644 index 0000000000..0146283ef9 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/NearestAncestorOrSelfDynamicRootQueryStep.cs @@ -0,0 +1,37 @@ +using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public class NearestAncestorOrSelfDynamicRootQueryStep : IDynamicRootQueryStep +{ + private readonly ICoreScopeProvider _scopeProvider; + private readonly IDynamicRootRepository _nodeFilterRepository; + + public NearestAncestorOrSelfDynamicRootQueryStep(ICoreScopeProvider scopeProvider, IDynamicRootRepository nodeFilterRepository) + { + _scopeProvider = scopeProvider; + _nodeFilterRepository = nodeFilterRepository; + } + + protected virtual string SupportedDirectionAlias { get; set; } = "NearestAncestorOrSelf"; + + public async Task>> ExecuteAsync(ICollection origins, DynamicRootQueryStep filter) + { + if (filter.Alias != SupportedDirectionAlias) + { + return Attempt>.Fail(); + } + + if (origins.Count < 1) + { + return Attempt>.Succeed(Array.Empty()); + } + + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + var result = (await _nodeFilterRepository.NearestAncestorOrSelfAsync(origins, filter))?.ToSingleItemCollection() ?? Array.Empty(); + + return Attempt>.Succeed(result); + } +} diff --git a/src/Umbraco.Core/DynamicRoot/QuerySteps/NearestDescendantOrSelfDynamicRootQueryStep.cs b/src/Umbraco.Core/DynamicRoot/QuerySteps/NearestDescendantOrSelfDynamicRootQueryStep.cs new file mode 100644 index 0000000000..1e36c79436 --- /dev/null +++ b/src/Umbraco.Core/DynamicRoot/QuerySteps/NearestDescendantOrSelfDynamicRootQueryStep.cs @@ -0,0 +1,35 @@ +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.DynamicRoot.QuerySteps; + +public class NearestDescendantOrSelfDynamicRootQueryStep : IDynamicRootQueryStep +{ + private readonly ICoreScopeProvider _scopeProvider; + private readonly IDynamicRootRepository _nodeFilterRepository; + + public NearestDescendantOrSelfDynamicRootQueryStep(ICoreScopeProvider scopeProvider, IDynamicRootRepository nodeFilterRepository) + { + _scopeProvider = scopeProvider; + _nodeFilterRepository = nodeFilterRepository; + } + + protected virtual string SupportedDirectionAlias { get; set; } = "NearestDescendantOrSelf"; + + public async Task>> ExecuteAsync(ICollection origins, DynamicRootQueryStep filter) + { + if (filter.Alias != SupportedDirectionAlias) + { + return Attempt>.Fail(); + } + + if (origins.Count < 1) + { + return Attempt>.Succeed(Array.Empty()); + } + + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + var result = await _nodeFilterRepository.NearestDescendantOrSelfAsync(origins, filter); + + return Attempt>.Succeed(result); + } +} diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index c24bbdcdd0..128ed70229 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -1004,6 +1004,12 @@ Umbraco %0% for en frisk installation eller for en opgradering fra version 3.0.

Tryk på Næste for at begynde på guiden.]]>
+ + Opret webhook + Tilføj webhook header + Tilføj dokument type + Tilføj medie Type + Culture Code Culture Name @@ -1215,6 +1221,41 @@ Mange hilsner fra Umbraco robotten Du kan kun vælge følgende type(r) dokumenter: %0% Du har valgt et dokument som er slettet eller lagt i papirkurven Du har valgt dokumenter som er slettede eller lagt i papirkurven + Afgræns udgangspunktet + Vælg udgangspunkt + Definer med XPath + Definer Dynamisk Udgangspunkt + + + Dynamisk udgangspunkts forespørgsel + Vælg begyndelsen + Beskriv begyndelsen for dynamisk udgangspunkts forespørgselen + Roden + Rod noden for denne kilde + Overliggende + Den overliggende node af kilden i denne redigerings session + Nuværende + Kilde noden for denne redigerings session + Siden + Nærmeste node med et domæne + Specifik Node + Vælg en specifik Node + Tilføj skridt til forespørgsel + Specificer næste skridt i din dynamisk udgangspunkts forespørgsel + Nærmeste forældre eller selv + Forespørg the nærmeste forældre eller selv der passer på en af de givne typer + Fjerneste forældre eller selv + Forespørg fjerneste forældre eller selv der passer på en af de givne typer + Nærmeste barn eller selv + Forespørg nærmeste barn eller selv der passer på en af de givne typer + Fjerneste barn eller selv + Forespørg fjerneste barn eller selv der passer på en af de givne typer + Brugerdefineret + Forespørg med et skræddersyet forespørgsels skridt + Tilføj skridt + der passer med typerne: + Intet passende indhold + Konfigurationen af dette felt passer ikke med noget indhold. Opret det manglende indhold eller kontakt din adminnistrator for at tilpasse Dynamisk Udgangspunkts Forespørgselen for dette felt. Slettet medie @@ -2279,6 +2320,8 @@ Mange hilsner fra Umbraco robotten Konfigurer område Slet område Tilføj mulighed for %0% koloner + Indsæt Blok + Vis på linje med tekst Hvad er Indholdsskabeloner? diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 8c35554043..9d1b110b8a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -1431,6 +1431,41 @@ To manage your website, simply open the Umbraco backoffice and start adding cont You can only select items of type(s): %0% You have picked a content item currently deleted or in the recycle bin You have picked content items currently deleted or in the recycle bin + Specify root + Pick root node + Specify via XPath + Specify a Dynamic Root + + + Dynamic Root Query + Pick origin + Define the origin for your Dynamic Root Query + Root + Root node of this editing session + Parent + The parent node of the source in this editing session + Current + The content node that is source for this editing session + Site + Find nearest node with a hostname + Specific Node + Pick a specific Node as the origin for this query + Append step to query + Define the next step of your Dynamic Root Query + Nearest Ancestor Or Self + Query the nearest ancestor or self that fits with one of the configured types + Furthest Ancestor Or Self + Query the Furthest ancestor or self that fits with one of the configured types + Nearest Descendant Or Self + Query the nearest descendant or self that fits with one of the configured types + Furthest Descendant Or Self + Query the Furthest descendant or self that fits with one of the configured types + Custom + Query the using a custom Query Step + Add query step + That matches types: + No matching content + The configuration of this property does not match any content. Create the missing content or contact your adminnistrator to adjust the Dynamic Root settings for this property. Deleted item @@ -2848,6 +2883,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Configure area Delete area Add spanning %0% columns option + Insert Block + Display inline with text What are Content Templates? diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 3369f61af6..77272aa79b 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1469,6 +1469,43 @@ To manage your website, simply open the Umbraco backoffice and start adding cont You can only select items of type(s): %0% You have picked a content item currently deleted or in the recycle bin You have picked content items currently deleted or in the recycle bin + Specify root + Pick root node + Specify root via XPath + Specify a Dynamic Root + Start node + XPath Query + + + Dynamic Root Query + Pick origin + Define the origin for your Dynamic Root Query + Root + Root node of this editing session + Parent + The parent node of the source in this editing session + Current + The content node that is source for this editing session + Site + Find nearest node with a hostname + Specific Node + Pick a specific Node as the origin for this query + Append step to query + Define the next step of your Dynamic Root Query + Nearest Ancestor Or Self + Query the nearest ancestor or self that fits with one of the configured types + Furthest Ancestor Or Self + Query the Furthest ancestor or self that fits with one of the configured types + Nearest Descendant Or Self + Query the nearest descendant or self that fits with one of the configured types + Furthest Descendant Or Self + Query the Furthest descendant or self that fits with one of the configured types + Custom + Query the using a custom Query Step + Add query step + That matches types: + No matching content + The configuration of this property does not match any content. Create the missing content or contact your adminnistrator to adjust the Dynamic Root settings for this property. Deleted item @@ -1975,6 +2012,13 @@ To manage your website, simply open the Umbraco backoffice and start adding cont NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]> Changing a data type with stored values is disabled. To allow this you can change the Umbraco:CMS:DataTypes:CanBeChanged setting in appsettings.json. + + Create webhook + Add webhook header + Logs + Add Document Type + Add Media Type + Add language ISO code @@ -2119,6 +2163,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Settings Templating Third Party + Webhooks New update ready @@ -2952,6 +2997,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Configure area Delete area Add spanning %0% columns option + Insert Block + Display inline with text What are Content Templates? diff --git a/src/Umbraco.Core/Extensions/CollectionExtensions.cs b/src/Umbraco.Core/Extensions/CollectionExtensions.cs new file mode 100644 index 0000000000..fd2f976d50 --- /dev/null +++ b/src/Umbraco.Core/Extensions/CollectionExtensions.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Extensions; + +public static class CollectionExtensions +{ + // Easiest way to return a collection with 1 item, probably not the most performant + public static ICollection ToSingleItemCollection(this T item) => + new T[] { item }; +} diff --git a/src/Umbraco.Core/Extensions/EnumerableExtensions.cs b/src/Umbraco.Core/Extensions/EnumerableExtensions.cs index e2c0936fa4..7bbb010e2d 100644 --- a/src/Umbraco.Core/Extensions/EnumerableExtensions.cs +++ b/src/Umbraco.Core/Extensions/EnumerableExtensions.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Collections; namespace Umbraco.Extensions; diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs new file mode 100644 index 0000000000..f5be6f9e23 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs @@ -0,0 +1,129 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Represents a layout item for the Block List editor. +/// +/// +[DataContract(Name = "block", Namespace = "")] +public class RichTextBlockItem : IBlockReference +{ + /// + /// Initializes a new instance of the class. + /// + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + /// + /// contentUdi + /// or + /// content + /// + public RichTextBlockItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) + { + ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + SettingsUdi = settingsUdi; + Settings = settings; + } + + /// + /// Gets the content. + /// + /// + /// The content. + /// + [DataMember(Name = "content")] + public IPublishedElement Content { get; } + + /// + /// Gets the settings UDI. + /// + /// + /// The settings UDI. + /// + [DataMember(Name = "settingsUdi")] + public Udi SettingsUdi { get; } + + /// + /// Gets the content UDI. + /// + /// + /// The content UDI. + /// + [DataMember(Name = "contentUdi")] + public Udi ContentUdi { get; } + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + [DataMember(Name = "settings")] + public IPublishedElement Settings { get; } +} + +/// +/// Represents a layout item with a generic content type for the Block List editor. +/// +/// The type of the content. +/// +public class RichTextBlockItem : RichTextBlockItem + where T : IPublishedElement +{ + /// + /// Initializes a new instance of the class. + /// + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + public RichTextBlockItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) + : base(contentUdi, content, settingsUdi, settings) => + Content = content; + + /// + /// Gets the content. + /// + /// + /// The content. + /// + public new T Content { get; } +} + +/// +/// Represents a layout item with generic content and settings types for the Block List editor. +/// +/// The type of the content. +/// The type of the settings. +/// +public class RichTextBlockItem : RichTextBlockItem + where TContent : IPublishedElement + where TSettings : IPublishedElement +{ + /// + /// Initializes a new instance of the class. + /// + /// The content udi. + /// The content. + /// The settings udi. + /// The settings. + public RichTextBlockItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) + : base(contentUdi, content, settingsUdi, settings) => + Settings = settings; + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + public new TSettings Settings { get; } +} diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs new file mode 100644 index 0000000000..13a184f325 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs @@ -0,0 +1,14 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Used for deserializing the rich text block layouts +/// +public class RichTextBlockLayoutItem : IBlockLayoutItem +{ + public Udi? ContentUdi { get; set; } + + public Udi? SettingsUdi { get; set; } +} diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockModel.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockModel.cs new file mode 100644 index 0000000000..76ed496684 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockModel.cs @@ -0,0 +1,38 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// The strongly typed model for blocks in the Rich Text editor. +/// +[DataContract(Name = "richTextEditorBlocks", Namespace = "")] +public class RichTextBlockModel : BlockModelCollection +{ + /// + /// Initializes a new instance of the class. + /// + /// The list to wrap. + public RichTextBlockModel(IList list) + : base(list) + { + } + + /// + /// Prevents a default instance of the class from being created. + /// + private RichTextBlockModel() + : this(new List()) + { + } + + /// + /// Gets the empty . + /// + /// + /// The empty . + /// + public static RichTextBlockModel Empty { get; } = new(); +} diff --git a/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs b/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs new file mode 100644 index 0000000000..12778eeacd --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Data converter for blocks in the richtext property editor +/// +internal sealed class RichTextEditorBlockDataConverter : BlockEditorDataConverter +{ + public RichTextEditorBlockDataConverter() + : base(Constants.PropertyEditors.Aliases.TinyMce) + { + } + + protected override IEnumerable? GetBlockReferences(JToken jsonLayout) + { + IEnumerable? blockListLayout = jsonLayout.ToObject>(); + return blockListLayout?.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); + } +} diff --git a/src/Umbraco.Core/Models/DefaultPayloadModel.cs b/src/Umbraco.Core/Models/DefaultPayloadModel.cs new file mode 100644 index 0000000000..45b2592b51 --- /dev/null +++ b/src/Umbraco.Core/Models/DefaultPayloadModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Models; + +internal class DefaultPayloadModel +{ + public Guid Id { get; set; } +} diff --git a/src/Umbraco.Core/Models/DeliveryApi/RichTextModel.cs b/src/Umbraco.Core/Models/DeliveryApi/RichTextModel.cs index 2af7570183..6280343c03 100644 --- a/src/Umbraco.Core/Models/DeliveryApi/RichTextModel.cs +++ b/src/Umbraco.Core/Models/DeliveryApi/RichTextModel.cs @@ -3,4 +3,8 @@ public class RichTextModel { public required string Markup { get; set; } + + public required IEnumerable Blocks { get; set; } + + public static RichTextModel Empty() => new() { Markup = string.Empty, Blocks = Array.Empty() }; } diff --git a/src/Umbraco.Core/Models/DeliveryApi/RichTextRootElement.cs b/src/Umbraco.Core/Models/DeliveryApi/RichTextRootElement.cs new file mode 100644 index 0000000000..8174d288d2 --- /dev/null +++ b/src/Umbraco.Core/Models/DeliveryApi/RichTextRootElement.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Cms.Core.Models.DeliveryApi; + +public sealed class RichTextRootElement : IRichTextElement +{ + public RichTextRootElement(Dictionary attributes, IEnumerable elements, IEnumerable blocks) + { + Attributes = attributes; + Elements = elements; + Blocks = blocks; + } + + public string Tag => "#root"; + + public Dictionary Attributes { get; } + + public IEnumerable Elements { get; } + + public IEnumerable Blocks { get; } +} diff --git a/src/Umbraco.Core/Models/Webhook.cs b/src/Umbraco.Core/Models/Webhook.cs new file mode 100644 index 0000000000..bc31745cf5 --- /dev/null +++ b/src/Umbraco.Core/Models/Webhook.cs @@ -0,0 +1,27 @@ +namespace Umbraco.Cms.Core.Models; + +public class Webhook +{ + public Webhook(string url, bool? enabled = null, Guid[]? entityKeys = null, string[]? events = null, IDictionary? headers = null) + { + Url = url; + Headers = headers ?? new Dictionary(); + Events = events ?? Array.Empty(); + ContentTypeKeys = entityKeys ?? Array.Empty(); + Enabled = enabled ?? false; + } + + public int Id { get; set; } + + public Guid Key { get; set; } + + public string Url { get; set; } + + public string[] Events { get; set; } + + public Guid[] ContentTypeKeys {get; set; } + + public bool Enabled { get; set; } + + public IDictionary Headers { get; set; } +} diff --git a/src/Umbraco.Core/Models/WebhookLog.cs b/src/Umbraco.Core/Models/WebhookLog.cs new file mode 100644 index 0000000000..bd37d79165 --- /dev/null +++ b/src/Umbraco.Core/Models/WebhookLog.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Cms.Core.Models; + +public class WebhookLog +{ + public int Id { get; set; } + + public Guid WebhookKey { get; set; } + + public Guid Key { get; set; } + + public string Url { get; set; } = string.Empty; + + public string StatusCode { get; set; } = string.Empty; + + public DateTime Date { get; set; } + + public string EventName { get; set; } = string.Empty; + + public int RetryCount { get; set; } + + public string RequestHeaders { get; set; } = string.Empty; + + public string? RequestBody { get; set; } = string.Empty; + + public string ResponseHeaders { get; set; } = string.Empty; + + public string ResponseBody { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Core/Models/WebhookResponseModel.cs b/src/Umbraco.Core/Models/WebhookResponseModel.cs new file mode 100644 index 0000000000..1f40443806 --- /dev/null +++ b/src/Umbraco.Core/Models/WebhookResponseModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Models; + +public class WebhookResponseModel +{ + public HttpResponseMessage? HttpResponseMessage { get; set; } + + public int RetryCount { get; set; } +} diff --git a/src/Umbraco.Core/PaginationHelper.cs b/src/Umbraco.Core/PaginationHelper.cs index 2fbf6ff771..eb9049c1da 100644 --- a/src/Umbraco.Core/PaginationHelper.cs +++ b/src/Umbraco.Core/PaginationHelper.cs @@ -2,7 +2,7 @@ public static class PaginationHelper { - internal static void ConvertSkipTakeToPaging(int skip, int take, out long pageNumber, out int pageSize) + public static void ConvertSkipTakeToPaging(int skip, int take, out long pageNumber, out int pageSize) { if (skip % take != 0) { diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index a7c094db2a..06b5d001a9 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -87,6 +87,11 @@ public static partial class Constants public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; + public const string Webhook = TableNamePrefix + "Webhook"; + public const string Webhook2ContentTypeKeys = Webhook + "2ContentTypeKeys"; + public const string Webhook2Events = Webhook + "2Events"; + public const string Webhook2Headers = Webhook + "2Headers"; + public const string WebhookLog = TableNamePrefix + "WebhookLog"; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs new file mode 100644 index 0000000000..a4652d5955 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IWebhookLogRepository +{ + Task CreateAsync(WebhookLog log); + + Task> GetPagedAsync(int skip, int take); +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs new file mode 100644 index 0000000000..d045cd172f --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IWebhookRepository +{ + /// + /// Gets all of the webhooks in the current database. + /// + /// Number of entries to skip. + /// Number of entries to take. + /// A paged model of objects. + Task> GetAllAsync(int skip, int take); + + /// + /// Gets all of the webhooks in the current database. + /// + /// The webhook you want to create. + /// The created webhook + Task CreateAsync(Webhook webhook); + + /// + /// Gets a webhook by key + /// + /// The key of the webhook which will be retrieved. + /// The webhook with the given key. + Task GetAsync(Guid key); + + /// + /// Gets a webhook by key + /// + /// The key of the webhook which will be retrieved. + /// The webhook with the given key. + Task> GetByEventNameAsync(string eventName); + + /// + /// Gets a webhook by key + /// + /// The webhook to be deleted. + /// A representing the asynchronous operation. + Task DeleteAsync(Webhook webhook); + + /// + /// Updates a given webhook + /// + /// The webhook to be updated. + /// The updated webhook. + Task UpdateAsync(Webhook webhook); +} diff --git a/src/Umbraco.Core/PropertyEditors/IRichTextPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/IRichTextPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..f48f7ad254 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IRichTextPropertyIndexValueFactory.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.PropertyEditors; + +public interface IRichTextPropertyIndexValueFactory : IPropertyIndexValueFactory +{ +} diff --git a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs index 2dcd0f6e93..ba6d605cca 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationTreeSource.cs @@ -14,6 +14,33 @@ public class MultiNodePickerConfigurationTreeSource [DataMember(Name = "query")] public string? StartNodeQuery { get; set; } + [DataMember(Name = "dynamicRoot")] + public DynamicRoot? DynamicRoot { get; set; } + [DataMember(Name = "id")] public Udi? StartNodeId { get; set; } } + +[DataContract] +public class DynamicRoot +{ + [DataMember(Name = "originAlias")] + public string OriginAlias { get; set; } = string.Empty; + + [DataMember(Name = "originKey")] + public Guid? OriginKey { get; set; } + + [DataMember(Name = "querySteps")] + public QueryStep[] QuerySteps { get; set; } = Array.Empty(); +} + +[DataContract] +public class QueryStep +{ + [DataMember(Name = "alias")] + public string Alias { get; set; } = string.Empty; + + [DataMember(Name = "anyOfDocTypeKeys")] + public IEnumerable AnyOfDocTypeKeys { get; set; } = Array.Empty(); +} + diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs index 6a80144d0d..3c028b9a39 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs @@ -1,3 +1,5 @@ +using System.Runtime.Serialization; + namespace Umbraco.Cms.Core.PropertyEditors; /// @@ -9,7 +11,13 @@ public class RichTextConfiguration : IIgnoreUserStartNodesConfig [ConfigurationField("editor", "Editor", "views/propertyeditors/rte/rte.prevalues.html", HideLabel = true)] public object? Editor { get; set; } - [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the overlay (link picker).")] + [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.html", Description = "Define the available blocks.")] + public RichTextBlockConfiguration[]? Blocks { get; set; } = null!; + + [ConfigurationField("useLiveEditing", "Blocks Live editing mode", "boolean", Description = "Live updated Block Elements when they are edited.")] + public bool UseLiveEditing { get; set; } + + [ConfigurationField("overlaySize", "Overlay Size", "overlaysize", Description = "Select the width of the link picker overlay.")] public string? OverlaySize { get; set; } [ConfigurationField("hideLabel", "Hide Label", "boolean")] @@ -24,4 +32,41 @@ public class RichTextConfiguration : IIgnoreUserStartNodesConfig "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } + + [DataContract] + public class RichTextBlockConfiguration : IBlockConfiguration + { + [DataMember(Name = "backgroundColor")] + public string? BackgroundColor { get; set; } + + [DataMember(Name = "iconColor")] + public string? IconColor { get; set; } + + [DataMember(Name = "thumbnail")] + public string? Thumbnail { get; set; } + + [DataMember(Name = "contentElementTypeKey")] + public Guid ContentElementTypeKey { get; set; } + + [DataMember(Name = "settingsElementTypeKey")] + public Guid? SettingsElementTypeKey { get; set; } + + [DataMember(Name = "view")] + public string? View { get; set; } + + [DataMember(Name = "stylesheet")] + public string? Stylesheet { get; set; } + + [DataMember(Name = "label")] + public string? Label { get; set; } + + [DataMember(Name = "editorSize")] + public string? EditorSize { get; set; } + + [DataMember(Name = "forceHideContentEditorInOverlay")] + public bool ForceHideContentEditorInOverlay { get; set; } + + [DataMember(Name = "displayInline")] + public bool DisplayInline { get; set; } + } } diff --git a/src/Umbraco.Core/Routing/IRedirectTracker.cs b/src/Umbraco.Core/Routing/IRedirectTracker.cs new file mode 100644 index 0000000000..2b0c8649a9 --- /dev/null +++ b/src/Umbraco.Core/Routing/IRedirectTracker.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Routing +{ + /// + /// Determines and records redirects for a content item following an update that may change it's public URL. + /// + public interface IRedirectTracker + { + /// + /// Stores the existing routes for a content item before update. + /// + /// The content entity updated. + /// The dictionary of routes for population. + void StoreOldRoute(IContent entity, Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes); + + /// + /// Creates appropriate redirects for the content item following an update. + /// + /// The populated dictionary of old routes; + void CreateRedirects(IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes); + } +} diff --git a/src/Umbraco.Core/Security/ContentPermissions.cs b/src/Umbraco.Core/Security/ContentPermissions.cs index d43e527a62..6c70326699 100644 --- a/src/Umbraco.Core/Security/ContentPermissions.cs +++ b/src/Umbraco.Core/Security/ContentPermissions.cs @@ -165,12 +165,7 @@ public class ContentPermissions throw new ArgumentNullException(nameof(user)); } - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } - - bool? hasPathAccess = null; + bool hasPathAccess; entity = null; if (nodeId == Constants.System.Root) @@ -181,19 +176,17 @@ public class ContentPermissions { hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); } - - if (hasPathAccess.HasValue) + else { - return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; - } + entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document); - entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document); - if (entity == null) - { - return ContentAccess.NotFound; - } + if (entity == null) + { + return ContentAccess.NotFound; + } - hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); + hasPathAccess = user.HasContentPathAccess(entity, _entityService, _appCaches); + } if (hasPathAccess == false) { @@ -206,7 +199,8 @@ public class ContentPermissions } // get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(entity.Path, user, permissionsToCheck) + // if there is no entity for this id, than just use the id as the path (i.e. -1 or -20) + return CheckPermissionsPath(entity?.Path ?? nodeId.ToString(), user, permissionsToCheck) ? ContentAccess.Granted : ContentAccess.Denied; } @@ -230,12 +224,7 @@ public class ContentPermissions throw new ArgumentNullException(nameof(user)); } - if (permissionsToCheck == null) - { - permissionsToCheck = Array.Empty(); - } - - bool? hasPathAccess = null; + bool hasPathAccess; contentItem = null; if (nodeId == Constants.System.Root) @@ -246,19 +235,17 @@ public class ContentPermissions { hasPathAccess = user.HasContentBinAccess(_entityService, _appCaches); } - - if (hasPathAccess.HasValue) + else { - return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; - } + contentItem = _contentService.GetById(nodeId); - contentItem = _contentService.GetById(nodeId); - if (contentItem == null) - { - return ContentAccess.NotFound; - } + if (contentItem == null) + { + return ContentAccess.NotFound; + } - hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches); + hasPathAccess = user.HasPathAccess(contentItem, _entityService, _appCaches); + } if (hasPathAccess == false) { @@ -271,7 +258,8 @@ public class ContentPermissions } // get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck) + // if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + return CheckPermissionsPath(contentItem?.Path ?? nodeId.ToString(), user, permissionsToCheck) ? ContentAccess.Granted : ContentAccess.Denied; } @@ -283,8 +271,7 @@ public class ContentPermissions permissionsToCheck = Array.Empty(); } - // get the implicit/inherited permissions for the user for this path, - // if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + // get the implicit/inherited permissions for the user for this path EntityPermissionSet permission = _userService.GetPermissionsForPath(user, path); var allowed = true; diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs new file mode 100644 index 0000000000..e63f07bf11 --- /dev/null +++ b/src/Umbraco.Core/Services/IWebHookService.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebHookService +{ + Task CreateAsync(Webhook webhook); + + Task UpdateAsync(Webhook webhook); + + Task DeleteAsync(Guid key); + + Task GetAsync(Guid key); + + Task> GetAllAsync(int skip, int take); + + Task> GetByEventNameAsync(string eventName); +} diff --git a/src/Umbraco.Core/Services/IWebhookFiringService.cs b/src/Umbraco.Core/Services/IWebhookFiringService.cs new file mode 100644 index 0000000000..0482290c3d --- /dev/null +++ b/src/Umbraco.Core/Services/IWebhookFiringService.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebhookFiringService +{ + Task FireAsync(Webhook webhook, string eventName, object? payload, CancellationToken cancellationToken); +} diff --git a/src/Umbraco.Core/Services/IWebhookLogFactory.cs b/src/Umbraco.Core/Services/IWebhookLogFactory.cs new file mode 100644 index 0000000000..fa600dda82 --- /dev/null +++ b/src/Umbraco.Core/Services/IWebhookLogFactory.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebhookLogFactory +{ + Task CreateAsync(string eventName, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken); +} diff --git a/src/Umbraco.Core/Services/IWebhookLogService.cs b/src/Umbraco.Core/Services/IWebhookLogService.cs new file mode 100644 index 0000000000..12b53bfa76 --- /dev/null +++ b/src/Umbraco.Core/Services/IWebhookLogService.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebhookLogService +{ + Task CreateAsync(WebhookLog webhookLog); + + Task> Get(int skip = 0, int take = int.MaxValue); +} diff --git a/src/Umbraco.Core/Services/WebhookLogFactory.cs b/src/Umbraco.Core/Services/WebhookLogFactory.cs new file mode 100644 index 0000000000..22dd75fe84 --- /dev/null +++ b/src/Umbraco.Core/Services/WebhookLogFactory.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Services; + +public class WebhookLogFactory : IWebhookLogFactory +{ + public async Task CreateAsync(string eventName, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken) + { + var log = new WebhookLog + { + Date = DateTime.UtcNow, + EventName = eventName, + Key = Guid.NewGuid(), + Url = webhook.Url, + WebhookKey = webhook.Key, + }; + + if (responseModel.HttpResponseMessage is not null) + { + log.RequestBody = await responseModel.HttpResponseMessage!.RequestMessage!.Content!.ReadAsStringAsync(cancellationToken); + log.ResponseBody = await responseModel.HttpResponseMessage.Content.ReadAsStringAsync(cancellationToken); + log.StatusCode = responseModel.HttpResponseMessage.StatusCode.ToString(); + log.RetryCount = responseModel.RetryCount; + log.ResponseHeaders = responseModel.HttpResponseMessage.Headers.ToString(); + log.RequestHeaders = responseModel.HttpResponseMessage.RequestMessage.Headers.ToString(); + } + + return log; + } +} diff --git a/src/Umbraco.Core/Services/WebhookLogService.cs b/src/Umbraco.Core/Services/WebhookLogService.cs new file mode 100644 index 0000000000..3b0bbebf19 --- /dev/null +++ b/src/Umbraco.Core/Services/WebhookLogService.cs @@ -0,0 +1,33 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Webhooks; + +namespace Umbraco.Cms.Core.Services; + +public class WebhookLogService : IWebhookLogService +{ + private readonly IWebhookLogRepository _webhookLogRepository; + private readonly ICoreScopeProvider _coreScopeProvider; + + public WebhookLogService(IWebhookLogRepository webhookLogRepository, ICoreScopeProvider coreScopeProvider) + { + _webhookLogRepository = webhookLogRepository; + _coreScopeProvider = coreScopeProvider; + } + + public async Task CreateAsync(WebhookLog webhookLog) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + await _webhookLogRepository.CreateAsync(webhookLog); + scope.Complete(); + + return webhookLog; + } + + public async Task> Get(int skip = 0, int take = int.MaxValue) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + return await _webhookLogRepository.GetPagedAsync(skip, take); + } +} diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs new file mode 100644 index 0000000000..5ccda00a0b --- /dev/null +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -0,0 +1,85 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services; + +public class WebhookService : IWebHookService +{ + private readonly ICoreScopeProvider _provider; + private readonly IWebhookRepository _webhookRepository; + + public WebhookService(ICoreScopeProvider provider, IWebhookRepository webhookRepository) + { + _provider = provider; + _webhookRepository = webhookRepository; + } + + public async Task CreateAsync(Webhook webhook) + { + using ICoreScope scope = _provider.CreateCoreScope(); + Webhook created = await _webhookRepository.CreateAsync(webhook); + scope.Complete(); + + return created; + } + + public async Task UpdateAsync(Webhook webhook) + { + using ICoreScope scope = _provider.CreateCoreScope(); + + Webhook? currentWebhook = await _webhookRepository.GetAsync(webhook.Key); + + if (currentWebhook is null) + { + throw new ArgumentException("Webhook does not exist"); + } + + currentWebhook.Enabled = webhook.Enabled; + currentWebhook.ContentTypeKeys = webhook.ContentTypeKeys; + currentWebhook.Events = webhook.Events; + currentWebhook.Url = webhook.Url; + currentWebhook.Headers = webhook.Headers; + + await _webhookRepository.UpdateAsync(currentWebhook); + scope.Complete(); + } + + public async Task DeleteAsync(Guid key) + { + using ICoreScope scope = _provider.CreateCoreScope(); + Webhook? webhook = await _webhookRepository.GetAsync(key); + if (webhook is not null) + { + await _webhookRepository.DeleteAsync(webhook); + } + + scope.Complete(); + } + + public async Task GetAsync(Guid key) + { + using ICoreScope scope = _provider.CreateCoreScope(); + Webhook? webhook = await _webhookRepository.GetAsync(key); + scope.Complete(); + return webhook; + } + + public async Task> GetAllAsync(int skip, int take) + { + using ICoreScope scope = _provider.CreateCoreScope(); + PagedModel webhooks = await _webhookRepository.GetAllAsync(skip, take); + scope.Complete(); + + return webhooks; + } + + public async Task> GetByEventNameAsync(string eventName) + { + using ICoreScope scope = _provider.CreateCoreScope(); + PagedModel webhooks = await _webhookRepository.GetByEventNameAsync(eventName); + scope.Complete(); + + return webhooks.Items; + } +} diff --git a/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs new file mode 100644 index 0000000000..629f47539a --- /dev/null +++ b/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Core.Webhooks.Events; + +public class ContentDeleteWebhookEvent : WebhookEventBase +{ + public ContentDeleteWebhookEvent( + IWebhookFiringService webhookFiringService, + IWebHookService webHookService, + IOptionsMonitor webhookSettings, + IServerRoleAccessor serverRoleAccessor) + : base( + webhookFiringService, + webHookService, + webhookSettings, + serverRoleAccessor, + Constants.WebhookEvents.ContentDelete) + { + } + + protected override IEnumerable GetEntitiesFromNotification(ContentDeletedNotification notification) => + notification.DeletedEntities; + + protected override object ConvertEntityToRequestPayload(IContent entity) => new DefaultPayloadModel { Id = entity.Key }; +} diff --git a/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs new file mode 100644 index 0000000000..4c75516420 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Core.Webhooks.Events; + +public class ContentPublishWebhookEvent : WebhookEventBase +{ + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IApiContentBuilder _apiContentBuilder; + + public ContentPublishWebhookEvent( + IWebhookFiringService webhookFiringService, + IWebHookService webHookService, + IOptionsMonitor webhookSettings, + IServerRoleAccessor serverRoleAccessor, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IApiContentBuilder apiContentBuilder) + : base( + webhookFiringService, + webHookService, + webhookSettings, + serverRoleAccessor, + Constants.WebhookEvents.ContentPublish) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _apiContentBuilder = apiContentBuilder; + } + + protected override IEnumerable GetEntitiesFromNotification(ContentPublishedNotification notification) => notification.PublishedEntities; + + protected override object? ConvertEntityToRequestPayload(IContent entity) + { + if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) is false || publishedSnapshot!.Content is null) + { + return null; + } + + IPublishedContent? publishedContent = publishedSnapshot.Content.GetById(entity.Key); + return publishedContent is null ? null : _apiContentBuilder.Build(publishedContent); + } +} diff --git a/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs new file mode 100644 index 0000000000..6c8fdf3598 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Core.Webhooks.Events; + +public class ContentUnpublishWebhookEvent : WebhookEventBase +{ + public ContentUnpublishWebhookEvent( + IWebhookFiringService webhookFiringService, + IWebHookService webHookService, + IOptionsMonitor webhookSettings, + IServerRoleAccessor serverRoleAccessor) + : base( + webhookFiringService, + webHookService, + webhookSettings, + serverRoleAccessor, + Constants.WebhookEvents.ContentUnpublish) + { + } + + protected override IEnumerable GetEntitiesFromNotification(ContentUnpublishedNotification notification) => notification.UnpublishedEntities; + + protected override object ConvertEntityToRequestPayload(IContent entity) => new DefaultPayloadModel { Id = entity.Key }; +} diff --git a/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs new file mode 100644 index 0000000000..51e1337f7d --- /dev/null +++ b/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Core.Webhooks.Events; + +public class MediaDeleteWebhookEvent : WebhookEventBase +{ + public MediaDeleteWebhookEvent( + IWebhookFiringService webhookFiringService, + IWebHookService webHookService, + IOptionsMonitor webhookSettings, + IServerRoleAccessor serverRoleAccessor) + : base( + webhookFiringService, + webHookService, + webhookSettings, + serverRoleAccessor, + Constants.WebhookEvents.MediaDelete) + { + } + + protected override IEnumerable GetEntitiesFromNotification(MediaDeletedNotification notification) => notification.DeletedEntities; + + protected override object ConvertEntityToRequestPayload(IMedia entity) => new DefaultPayloadModel { Id = entity.Key }; +} diff --git a/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs new file mode 100644 index 0000000000..d5a4dc57c5 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Core.Webhooks.Events; + +public class MediaSaveWebhookEvent : WebhookEventBase +{ + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IApiMediaBuilder _apiMediaBuilder; + + public MediaSaveWebhookEvent( + IWebhookFiringService webhookFiringService, + IWebHookService webHookService, + IOptionsMonitor webhookSettings, + IServerRoleAccessor serverRoleAccessor, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IApiMediaBuilder apiMediaBuilder) + : base( + webhookFiringService, + webHookService, + webhookSettings, + serverRoleAccessor, + Constants.WebhookEvents.MediaSave) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _apiMediaBuilder = apiMediaBuilder; + } + + protected override IEnumerable GetEntitiesFromNotification(MediaSavedNotification notification) => notification.SavedEntities; + + protected override object? ConvertEntityToRequestPayload(IMedia entity) + { + if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) is false || publishedSnapshot!.Content is null) + { + return null; + } + + IPublishedContent? publishedContent = publishedSnapshot.Media?.GetById(entity.Key); + return publishedContent is null ? null : _apiMediaBuilder.Build(publishedContent); + } +} diff --git a/src/Umbraco.Core/Webhooks/IWebhookEvent.cs b/src/Umbraco.Core/Webhooks/IWebhookEvent.cs new file mode 100644 index 0000000000..85857c1aec --- /dev/null +++ b/src/Umbraco.Core/Webhooks/IWebhookEvent.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Webhooks; + +public interface IWebhookEvent +{ + string EventName { get; set; } +} diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs new file mode 100644 index 0000000000..01384ea43f --- /dev/null +++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Core.Webhooks; + +public abstract class WebhookEventBase : IWebhookEvent, INotificationAsyncHandler + where TNotification : INotification + where TEntity : IContentBase +{ + private readonly IWebhookFiringService _webhookFiringService; + private readonly IWebHookService _webHookService; + private readonly IServerRoleAccessor _serverRoleAccessor; + private WebhookSettings _webhookSettings; + + protected WebhookEventBase( + IWebhookFiringService webhookFiringService, + IWebHookService webHookService, + IOptionsMonitor webhookSettings, + IServerRoleAccessor serverRoleAccessor, + string eventName) + { + _webhookFiringService = webhookFiringService; + _webHookService = webHookService; + _serverRoleAccessor = serverRoleAccessor; + EventName = eventName; + _webhookSettings = webhookSettings.CurrentValue; + webhookSettings.OnChange(x => _webhookSettings = x); + } + + public string EventName { get; set; } + + public virtual async Task HandleAsync(TNotification notification, CancellationToken cancellationToken) + { + if (_serverRoleAccessor.CurrentServerRole is not ServerRole.Single && _serverRoleAccessor.CurrentServerRole is not ServerRole.SchedulingPublisher) + { + return; + } + + if (_webhookSettings.Enabled is false) + { + return; + } + + IEnumerable webhooks = await _webHookService.GetByEventNameAsync(EventName); + + foreach (Webhook webhook in webhooks) + { + if (!webhook.Enabled) + { + continue; + } + + foreach (TEntity entity in GetEntitiesFromNotification(notification)) + { + if (webhook.ContentTypeKeys.Any() && !webhook.ContentTypeKeys.Contains(entity.ContentType.Key)) + { + continue; + } + + await _webhookFiringService.FireAsync(webhook, EventName, ConvertEntityToRequestPayload(entity), cancellationToken); + } + } + } + + protected abstract IEnumerable GetEntitiesFromNotification(TNotification notification); + + protected abstract object? ConvertEntityToRequestPayload(TEntity entity); +} diff --git a/src/Umbraco.Core/Webhooks/WebhookEventCollection.cs b/src/Umbraco.Core/Webhooks/WebhookEventCollection.cs new file mode 100644 index 0000000000..cf939f93ae --- /dev/null +++ b/src/Umbraco.Core/Webhooks/WebhookEventCollection.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Webhooks; + +public class WebhookEventCollection : BuilderCollectionBase +{ + public WebhookEventCollection(Func> items) : base(items) + { + } +} diff --git a/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs b/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs new file mode 100644 index 0000000000..e0eeb186e8 --- /dev/null +++ b/src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Webhooks.Events; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Webhooks; + +public class WebhookEventCollectionBuilder : OrderedCollectionBuilderBase +{ + protected override WebhookEventCollectionBuilder This => this; + + public override void RegisterWith(IServiceCollection services) + { + // register the collection + services.Add(new ServiceDescriptor(typeof(WebhookEventCollection), CreateCollection, ServiceLifetime.Singleton)); + + // register the types + RegisterTypes(services); + base.RegisterWith(services); + } + + public WebhookEventCollectionBuilder AddCoreWebhooks() + { + Append(); + Append(); + Append(); + Append(); + Append(); + return this; + } + + private void RegisterTypes(IServiceCollection services) + { + Type[] types = GetRegisteringTypes(GetTypes()).ToArray(); + + // ensure they are safe + foreach (Type type in types) + { + EnsureType(type, "register"); + } + + foreach (Type type in types) + { + Type? notificationType = GetNotificationType(type); + + if (notificationType is null) + { + continue; + } + + var descriptor = new ServiceDescriptor( + typeof(INotificationAsyncHandler<>).MakeGenericType(notificationType), + type, + ServiceLifetime.Transient); + + if (!services.Contains(descriptor)) + { + services.Add(descriptor); + } + } + } + + private Type? GetNotificationType(Type handlerType) + { + if (handlerType.IsOfGenericType(typeof(INotificationAsyncHandler<>))) + { + Type[] genericArguments = handlerType.BaseType!.GetGenericArguments(); + + Type? notificationType = genericArguments.FirstOrDefault(arg => typeof(INotification).IsAssignableFrom(arg)); + + if (notificationType is not null) + { + return notificationType; + } + } + + return null; + } +} diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs index c40debd690..eeb279e1b7 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs @@ -1,9 +1,14 @@ using HtmlAgilityPack; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Infrastructure.Extensions; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DeliveryApi; @@ -11,29 +16,51 @@ namespace Umbraco.Cms.Infrastructure.DeliveryApi; internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRichTextElementParser { private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IApiElementBuilder _apiElementBuilder; private readonly ILogger _logger; private const string TextNodeName = "#text"; + private const string CommentNodeName = "#comment"; + [Obsolete($"Please use the constructor that accepts {nameof(IApiElementBuilder)}. Will be removed in V15.")] public ApiRichTextElementParser( IApiContentRouteBuilder apiContentRouteBuilder, IPublishedUrlProvider publishedUrlProvider, IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger logger) + : this( + apiContentRouteBuilder, + publishedUrlProvider, + publishedSnapshotAccessor, + StaticServiceProvider.Instance.GetRequiredService(), + logger) + { + } + + public ApiRichTextElementParser( + IApiContentRouteBuilder apiContentRouteBuilder, + IPublishedUrlProvider publishedUrlProvider, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IApiElementBuilder apiElementBuilder, + ILogger logger) : base(apiContentRouteBuilder, publishedUrlProvider) { _publishedSnapshotAccessor = publishedSnapshotAccessor; + _apiElementBuilder = apiElementBuilder; _logger = logger; } - public IRichTextElement? Parse(string html) + [Obsolete($"Please use the overload that accepts {nameof(RichTextBlockModel)}. Will be removed in V15.")] + public IRichTextElement? Parse(string html) => Parse(html, null); + + public IRichTextElement? Parse(string html, RichTextBlockModel? richTextBlockModel) { try { IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); var doc = new HtmlDocument(); doc.LoadHtml(html); - return ParseRecursively(doc.DocumentNode, publishedSnapshot); + return ParseRootElement(doc.DocumentNode, publishedSnapshot, richTextBlockModel); } catch (Exception ex) { @@ -44,8 +71,8 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich private IRichTextElement ParseRecursively(HtmlNode current, IPublishedSnapshot publishedSnapshot) => current.Name == TextNodeName - ? ParseTextElement(current) - : ParseElement(current, publishedSnapshot); + ? ParseTextElement(current) + : ParseGenericElement(current, publishedSnapshot); private RichTextTextElement ParseTextElement(HtmlNode element) { @@ -57,16 +84,40 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich return new RichTextTextElement(element.InnerText); } - private RichTextGenericElement ParseElement(HtmlNode element, IPublishedSnapshot publishedSnapshot) + private RichTextRootElement ParseRootElement(HtmlNode element, IPublishedSnapshot publishedSnapshot, RichTextBlockModel? richTextBlockModel) + { + ApiBlockItem[] blocks = richTextBlockModel is not null + ? richTextBlockModel.Select(item => item.CreateApiBlockItem(_apiElementBuilder)).ToArray() + : Array.Empty(); + + return ParseElement( + element, + publishedSnapshot, + (_, attributes, childElements) => new RichTextRootElement(attributes, childElements, blocks)); + } + + private RichTextGenericElement ParseGenericElement(HtmlNode element, IPublishedSnapshot publishedSnapshot) { if (element.Name == TextNodeName) { throw new ArgumentException($"{TextNodeName} elements should be handled by {nameof(ParseTextElement)}"); } - // grab all non-#text nodes + all non-empty #text nodes as valid node children + return ParseElement( + element, + publishedSnapshot, + (tag, attributes, childElements) => new RichTextGenericElement(tag, attributes, childElements)); + } + + private T ParseElement(HtmlNode element, IPublishedSnapshot publishedSnapshot, Func, IRichTextElement[], T> createElement) + where T : IRichTextElement + { + // grab all valid node children: + // - non-#comment nodes + // - non-#text nodes + // - non-empty #text nodes HtmlNode[] childNodes = element.ChildNodes - .Where(c => c.Name != TextNodeName || string.IsNullOrWhiteSpace(c.InnerText) is false) + .Where(c => c.Name != CommentNodeName && (c.Name != TextNodeName || string.IsNullOrWhiteSpace(c.InnerText) is false)) .ToArray(); var tag = TagName(element); @@ -76,16 +127,18 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich ReplaceLocalImages(publishedSnapshot, tag, attributes); + CleanUpBlocks(tag, attributes); + SanitizeAttributes(attributes); IRichTextElement[] childElements = childNodes.Any() ? childNodes.Select(child => ParseRecursively(child, publishedSnapshot)).ToArray() : Array.Empty(); - return new RichTextGenericElement(tag, attributes, childElements); + return createElement(tag, attributes, childElements); } - private string TagName(HtmlNode htmlNode) => htmlNode.Name == "#document" ? "#root" : htmlNode.Name; + private string TagName(HtmlNode htmlNode) => htmlNode.Name; private void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, Dictionary attributes) { @@ -120,6 +173,22 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich }); } + private void CleanUpBlocks(string tag, Dictionary attributes) + { + if (tag.StartsWith("umb-rte-block") is false || attributes.ContainsKey("data-content-udi") is false || attributes["data-content-udi"] is not string dataUdi) + { + return; + } + + if (UdiParser.TryParse(dataUdi, out GuidUdi? guidUdi) is false) + { + return; + } + + attributes["content-id"] = guidUdi.Guid; + attributes.Remove("data-content-udi"); + } + private static void SanitizeAttributes(Dictionary attributes) { KeyValuePair[] dataAttributes = attributes diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index 04344905e4..f7eeba0f18 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -1,5 +1,6 @@ using HtmlAgilityPack; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; @@ -35,6 +36,8 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT ReplaceLocalImages(doc, publishedSnapshot); + CleanUpBlocks(doc); + return doc.DocumentNode.InnerHtml; } catch (Exception ex) @@ -91,4 +94,24 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT }); } } + + private void CleanUpBlocks(HtmlDocument doc) + { + HtmlNode[] blocks = doc.DocumentNode.SelectNodes("//*[starts-with(local-name(),'umb-rte-block')]")?.ToArray() ?? Array.Empty(); + foreach (HtmlNode block in blocks) + { + var dataUdi = block.GetAttributeValue("data-content-udi", string.Empty); + if (UdiParser.TryParse(dataUdi, out GuidUdi? guidUdi) is false) + { + continue; + } + + // swap the content UDI for the content ID + block.Attributes.Remove("data-content-udi"); + block.SetAttributeValue("data-content-id", guidUdi.Guid.ToString("D")); + + // remove the inner comment placed by the RTE + block.RemoveAllChildren(); + } + } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index b7a69f4323..909dd7704a 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -40,6 +40,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Infrastructure.DeliveryApi; using Umbraco.Cms.Infrastructure.DistributedLocking; using Umbraco.Cms.Infrastructure.Examine; @@ -54,6 +55,7 @@ using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Infrastructure.Routing; using Umbraco.Cms.Infrastructure.Runtime; using Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; using Umbraco.Cms.Infrastructure.Scoping; @@ -223,6 +225,9 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddTransient(); + + builder.Services.AddSingleton(); + builder.AddInstaller(); // Services required to run background jobs (with out the handler) @@ -233,6 +238,7 @@ public static partial class UmbracoBuilderExtensions builder.AddPropertyIndexValueFactories(); builder.AddDeliveryApiCoreServices(); + builder.Services.AddTransient(); return builder; } @@ -242,6 +248,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/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 2043d741fa..aadf24c79e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -2,6 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.DynamicRoot; +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; using Umbraco.Cms.Infrastructure.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Services.Implement; @@ -71,6 +73,11 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); return builder; diff --git a/src/Umbraco.Infrastructure/Extensions/DeliveryApiBlockReferenceExtensions.cs b/src/Umbraco.Infrastructure/Extensions/DeliveryApiBlockReferenceExtensions.cs new file mode 100644 index 0000000000..70ecdaee51 --- /dev/null +++ b/src/Umbraco.Infrastructure/Extensions/DeliveryApiBlockReferenceExtensions.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.DeliveryApi; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Infrastructure.Extensions; + +internal static class DeliveryApiBlockReferenceExtensions +{ + internal static ApiBlockItem CreateApiBlockItem( + this IBlockReference blockItem, + IApiElementBuilder apiElementBuilder) + => new ApiBlockItem( + apiElementBuilder.Build(blockItem.Content), + blockItem.Settings != null ? apiElementBuilder.Build(blockItem.Settings) : null); +} diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index 81afad16f8..b983f3e663 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -126,7 +126,7 @@ namespace Umbraco.Extensions /// A Serilog LoggerConfiguration /// /// The log level you wish the JSON file to collect - default is Verbose (highest) - /// + /// [Obsolete("Will be removed in Umbraco 13.")] public static LoggerConfiguration OutputDefaultTextFile( this LoggerConfiguration logConfig, diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 1f940b594d..c13389f52b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -83,7 +83,12 @@ public class DatabaseSchemaCreator typeof(ContentVersionCleanupPolicyDto), typeof(UserGroup2NodeDto), typeof(CreatedPackageSchemaDto), - typeof(UserGroup2LanguageDto) + typeof(UserGroup2LanguageDto), + typeof(WebhookDto), + typeof(Webhook2ContentTypeKeysDto), + typeof(Webhook2EventsDto), + typeof(Webhook2HeadersDto), + typeof(WebhookLogDto), }; private readonly IUmbracoDatabase _database; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index a387f4d06d..11c2736228 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -96,6 +96,9 @@ public class UmbracoPlan : MigrationPlan // And once more for 12 To("{2D4C9FBD-08B3-472D-A76C-6ED467A0CD20}"); + // To 13.0.0 + To("{C76D9C9A-635B-4D2C-A301-05642A523E9D}"); + // To 14.0.0 To("{419827A0-4FCE-464B-A8F3-247C6092AF55}"); To("{5F15A1CC-353D-4889-8C7E-F303B4766196}"); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhooks.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhooks.cs new file mode 100644 index 0000000000..e8026ea34d --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhooks.cs @@ -0,0 +1,41 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; + +public class AddWebhooks : MigrationBase +{ + public AddWebhooks(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Webhook) is false) + { + Create.Table().Do(); + } + + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Webhook2Events) is false) + { + Create.Table().Do(); + } + + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Webhook2ContentTypeKeys) is false) + { + Create.Table().Do(); + } + + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Webhook2Headers) is false) + { + Create.Table().Do(); + } + + if (tables.InvariantContains(Constants.DatabaseSchema.Tables.WebhookLog) is false) + { + Create.Table().Do(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs new file mode 100644 index 0000000000..350ce31ab2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs @@ -0,0 +1,68 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Converts the block json data into objects +/// +public abstract class BlockEditorDataConverter +{ + private readonly string _propertyEditorAlias; + + protected BlockEditorDataConverter(string propertyEditorAlias) => _propertyEditorAlias = propertyEditorAlias; + + public BlockEditorData ConvertFrom(JToken json) + { + BlockValue? value = json.ToObject(); + return Convert(value); + } + + public bool TryDeserialize(string json, [MaybeNullWhen(false)] out BlockEditorData blockEditorData) + { + try + { + BlockValue? value = JsonConvert.DeserializeObject(json); + blockEditorData = Convert(value); + return true; + } + catch (Exception) + { + blockEditorData = null; + return false; + } + } + + public BlockEditorData Deserialize(string json) + { + BlockValue? value = JsonConvert.DeserializeObject(json); + return Convert(value); + } + + public BlockEditorData Convert(BlockValue? value) + { + if (value?.Layout == null) + { + return BlockEditorData.Empty; + } + + IEnumerable? references = + value.Layout.TryGetValue(_propertyEditorAlias, out JToken? layout) + ? GetBlockReferences(layout) + : Enumerable.Empty(); + + return new BlockEditorData(_propertyEditorAlias, references!, value); + } + + /// + /// Return the collection of from the block editor's Layout (which could be an array or + /// an object depending on the editor) + /// + /// + /// + protected abstract IEnumerable? GetBlockReferences(JToken jsonLayout); +} diff --git a/src/Umbraco.Infrastructure/Models/RichTextEditorValue.cs b/src/Umbraco.Infrastructure/Models/RichTextEditorValue.cs new file mode 100644 index 0000000000..11754ccc3b --- /dev/null +++ b/src/Umbraco.Infrastructure/Models/RichTextEditorValue.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Core; + +[DataContract] +public class RichTextEditorValue +{ + [DataMember(Name = "markup")] + public required string Markup { get; set; } + + [DataMember(Name = "blocks")] + public required BlockValue? Blocks { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2ContentTypeKeysDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2ContentTypeKeysDto.cs new file mode 100644 index 0000000000..71bbed5962 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2ContentTypeKeysDto.cs @@ -0,0 +1,20 @@ +using System.Data; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + + +[TableName(Constants.DatabaseSchema.Tables.Webhook2ContentTypeKeys)] +[ExplicitColumns] +public class Webhook2ContentTypeKeysDto +{ + [Column("webhookId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_webhookEntityKey2Webhook", OnColumns = "webhookId, entityKey")] + [ForeignKey(typeof(WebhookDto), OnDelete = Rule.Cascade)] + public int WebhookId { get; set; } + + [Column("entityKey")] + public Guid ContentTypeKey { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2EventsDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2EventsDto.cs new file mode 100644 index 0000000000..0278d22945 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2EventsDto.cs @@ -0,0 +1,18 @@ +using System.Data; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Webhook2Events)] +public class Webhook2EventsDto +{ + [Column("webhookId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_webhookEvent2WebhookDto", OnColumns = "webhookId, event")] + [ForeignKey(typeof(WebhookDto), OnDelete = Rule.Cascade)] + public int WebhookId { get; set; } + + [Column("event")] + public string Event { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2HeadersDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2HeadersDto.cs new file mode 100644 index 0000000000..80a7724109 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2HeadersDto.cs @@ -0,0 +1,21 @@ +using System.Data; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Webhook2Headers)] +public class Webhook2HeadersDto +{ + [Column("webhookId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_heaeders2WebhookDto", OnColumns = "webhookId, key")] + [ForeignKey(typeof(WebhookDto), OnDelete = Rule.Cascade)] + public int WebhookId { get; set; } + + [Column("Key")] + public string Key { get; set; } = string.Empty; + + [Column("Value")] + public string Value { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs new file mode 100644 index 0000000000..abcf160b03 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs @@ -0,0 +1,40 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + + +[TableName(Constants.DatabaseSchema.Tables.Webhook)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class WebhookDto +{ + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = true)] + public int Id { get; set; } + + [Column(Name = "key")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid Key { get; set; } + + [Column(Name = "url")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Url { get; set; } = string.Empty; + + [Column(Name = "enabled")] + public bool Enabled { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = nameof(Webhook2EventsDto.WebhookId))] + public List Webhook2Events { get; set; } = new(); + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = nameof(Webhook2ContentTypeKeysDto.WebhookId))] + public List Webhook2ContentTypeKeys { get; set; } = new(); + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = nameof(Webhook2HeadersDto.WebhookId))] + public List Webhook2Headers { get; set; } = new(); +} + diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs new file mode 100644 index 0000000000..a8606c7391 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs @@ -0,0 +1,59 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.WebhookLog)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class WebhookLogDto +{ + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = true)] + public int Id { get; set; } + + [Column("webhookId")] + public Guid WebhookKey { get; set; } + + [Column(Name = "key")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid Key { get; set; } + + [Column(Name = "statusCode")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string StatusCode { get; set; } = string.Empty; + + [Column(Name = "date")] + [Index(IndexTypes.NonClustered, Name = "IX_" + Constants.DatabaseSchema.Tables.WebhookLog + "_date")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime Date { get; set; } + + [Column(Name = "url")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Url { get; set; } = string.Empty; + + [Column(Name = "eventName")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string EventName { get; set; } = string.Empty; + + [Column(Name = "retryCount")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int RetryCount { get; set; } + + [Column(Name = "requestHeaders")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string RequestHeaders { get; set; } = string.Empty; + + [Column(Name = "requestBody")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string RequestBody { get; set; } = string.Empty; + + [Column(Name = "responseHeaders")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string ResponseHeaders { get; set; } = string.Empty; + + [Column(Name = "responseBody")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string ResponseBody { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs new file mode 100644 index 0000000000..9e6328501f --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs @@ -0,0 +1,58 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class WebhookFactory +{ + public static Webhook BuildEntity(WebhookDto dto, IEnumerable? entityKey2WebhookDtos = null, IEnumerable? event2WebhookDtos = null, IEnumerable? headersWebhookDtos = null) + { + var entity = new Webhook( + dto.Url, + dto.Enabled, + entityKey2WebhookDtos?.Select(x => x.ContentTypeKey).ToArray(), + event2WebhookDtos?.Select(x => x.Event).ToArray(), + headersWebhookDtos?.ToDictionary(x => x.Key, x => x.Value)) + { + Id = dto.Id, + Key = dto.Key, + }; + + return entity; + } + + public static WebhookDto BuildDto(Webhook webhook) + { + var dto = new WebhookDto + { + Url = webhook.Url, + Key = webhook.Key, + Enabled = webhook.Enabled, + Id = webhook.Id, + }; + + return dto; + } + + public static IEnumerable BuildEntityKey2WebhookDto(Webhook webhook) => + webhook.ContentTypeKeys.Select(x => new Webhook2ContentTypeKeysDto + { + ContentTypeKey = x, + WebhookId = webhook.Id, + }); + + public static IEnumerable BuildEvent2WebhookDto(Webhook webhook) => + webhook.Events.Select(x => new Webhook2EventsDto + { + Event = x, + WebhookId = webhook.Id, + }); + + public static IEnumerable BuildHeaders2WebhookDtos(Webhook webhook) => + webhook.Headers.Select(x => new Webhook2HeadersDto + { + Key = x.Key, + Value = x.Value, + WebhookId = webhook.Id, + }); +} diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs new file mode 100644 index 0000000000..2cc6d5d55b --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs @@ -0,0 +1,42 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class WebhookLogFactory +{ + public static WebhookLogDto CreateDto(WebhookLog log) => + new() + { + Date = log.Date, + EventName = log.EventName, + RequestBody = log.RequestBody ?? string.Empty, + ResponseBody = log.ResponseBody, + RetryCount = log.RetryCount, + StatusCode = log.StatusCode, + Key = log.Key, + Id = log.Id, + Url = log.Url, + RequestHeaders = log.RequestHeaders, + ResponseHeaders = log.ResponseHeaders, + WebhookKey = log.WebhookKey, + }; + + public static WebhookLog DtoToEntity(WebhookLogDto dto) => + new() + { + Date = dto.Date, + EventName = dto.EventName, + RequestBody = dto.RequestBody, + ResponseBody = dto.ResponseBody, + RetryCount = dto.RetryCount, + StatusCode = dto.StatusCode, + Key = dto.Key, + Id = dto.Id, + Url = dto.Url, + RequestHeaders = dto.RequestHeaders, + ResponseHeaders = dto.ResponseHeaders, + WebhookKey = dto.WebhookKey, + }; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DynamicRootRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DynamicRootRepository.cs new file mode 100644 index 0000000000..b634f45125 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DynamicRootRepository.cs @@ -0,0 +1,125 @@ +using NPoco; +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class DynamicRootRepository: IDynamicRootRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public DynamicRootRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + private IUmbracoDatabase Database + { + get + { + if (_scopeAccessor.AmbientScope is null) + { + throw new NotSupportedException("Need to be executed in a scope"); + } + + return _scopeAccessor.AmbientScope.Database; + } + } + + public async Task NearestAncestorOrSelfAsync(IEnumerable origins, DynamicRootQueryStep filter) + { + Sql query = Database.SqlContext.SqlSyntax.SelectTop( + GetAncestorOrSelfBaseQuery(origins, filter) + .Append($"ORDER BY n.level DESC"), + 1); + + return await Database.SingleOrDefaultAsync(query); + } + + public async Task FarthestAncestorOrSelfAsync(IEnumerable origins, DynamicRootQueryStep filter) { + Sql query = Database.SqlContext.SqlSyntax.SelectTop( + GetAncestorOrSelfBaseQuery(origins, filter) + .Append($"ORDER BY n.level ASC"), + 1); + + return await Database.SingleOrDefaultAsync(query); + } + + private Sql GetAncestorOrSelfBaseQuery(IEnumerable origins, DynamicRootQueryStep filter) + { + var query = Database.SqlContext.Sql() + .Select("n", n => n.UniqueId) + .From("norigin") + .Append( // hack because npoco do not support this + $"INNER JOIN {Database.SqlContext.SqlSyntax.GetQuotedTableName(NodeDto.TableName)} n ON {Database.SqlContext.SqlSyntax.Substring}(norigin.path, 1, {Database.SqlContext.SqlSyntax.Length}(n.path)) = n.path") + .InnerJoin("c") + .On((c, n) => c.NodeId == n.NodeId, "c", "n") + .InnerJoin("ct") + .On((c, ct) => c.ContentTypeId == ct.NodeId, "c", "ct") + .InnerJoin("ctn") + .On((ct, ctn) => ct.NodeId == ctn.NodeId, "ct", "ctn") + .Where(norigin => origins.Contains(norigin.UniqueId), "norigin"); + + if (filter.AnyOfDocTypeKeys.Any()) + { + query = query.Where(ctn => filter.AnyOfDocTypeKeys.Contains(ctn.UniqueId), "ctn"); + } + + return query; + } + + + public async Task> NearestDescendantOrSelfAsync(ICollection origins, DynamicRootQueryStep filter) + { + var level = Database.Single(Database.SqlContext.Sql() + .Select("COALESCE(MIN(n.level), 0)") + .DescendantOrSelfBaseQuery(origins, filter)); + + Sql query = + Database.SqlContext.Sql() + .Select("n", n => n.UniqueId) + .DescendantOrSelfBaseQuery(origins, filter) + .Where(n => n.Level == level, "n"); + + return await Database.FetchAsync(query); + } + + public async Task> FarthestDescendantOrSelfAsync(ICollection origins, DynamicRootQueryStep filter) + { + var level = Database.Single(Database.SqlContext.Sql() + .Select("COALESCE(MAX(n.level), 0)") + .DescendantOrSelfBaseQuery(origins, filter)); + + Sql query = + Database.SqlContext.Sql() + .Select("n", n => n.UniqueId) + .DescendantOrSelfBaseQuery(origins, filter) + .Where(n => n.Level == level, "n"); + + return await Database.FetchAsync(query); + } +} + +internal static class HelperExtensions +{ + internal static Sql DescendantOrSelfBaseQuery(this Sql sql, IEnumerable origins, DynamicRootQueryStep filter) + { + var query = sql + .From("norigin") + .Append(// hack because npoco do not support this + $"INNER JOIN {sql.SqlContext.SqlSyntax.GetQuotedTableName(NodeDto.TableName)} n ON {sql.SqlContext.SqlSyntax.Substring}(N.path, 1, {sql.SqlContext.SqlSyntax.Length}(norigin.path)) = norigin.path") + .InnerJoin("c") + .On((c, n) => c.NodeId == n.NodeId, "c", "n") + .InnerJoin("ct") + .On((c, ct) => c.ContentTypeId == ct.NodeId, "c", "ct") + .InnerJoin("ctn") + .On((ct, ctn) => ct.NodeId == ctn.NodeId, "ct", "ctn") + .Where(norigin => origins.Contains(norigin.UniqueId), "norigin"); + + if (filter.AnyOfDocTypeKeys.Any()) + { + query = query.Where(ctn => filter.AnyOfDocTypeKeys.Contains(ctn.UniqueId), "ctn"); + } + + return query; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs new file mode 100644 index 0000000000..910f1178d4 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs @@ -0,0 +1,44 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class WebhookLogRepository : IWebhookLogRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public WebhookLogRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + public async Task CreateAsync(WebhookLog log) + { + WebhookLogDto dto = WebhookLogFactory.CreateDto(log); + var result = await _scopeAccessor.AmbientScope?.Database.InsertAsync(dto)!; + var id = Convert.ToInt32(result); + log.Id = id; + } + + public async Task> GetPagedAsync(int skip, int take) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From() + .OrderByDescending(x => x.Date); + + PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); + + Page? page = await _scopeAccessor.AmbientScope?.Database.PageAsync(pageNumber + 1, pageSize, sql)!; + + return new PagedModel + { + Total = page.TotalItems, + Items = page.Items.Select(WebhookLogFactory.DtoToEntity), + }; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs new file mode 100644 index 0000000000..af6d651e44 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs @@ -0,0 +1,136 @@ +using NPoco; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class WebhookRepository : IWebhookRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public WebhookRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + public async Task> GetAllAsync(int skip, int take) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From(); + + List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; + + return new PagedModel + { + Items = await DtosToEntities(webhookDtos.Skip(skip).Take(take)), + Total = webhookDtos.Count, + }; + } + + public async Task CreateAsync(Webhook webhook) + { + WebhookDto webhookDto = WebhookFactory.BuildDto(webhook); + + var result = await _scopeAccessor.AmbientScope?.Database.InsertAsync(webhookDto)!; + + var id = Convert.ToInt32(result); + webhook.Id = id; + + await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(WebhookFactory.BuildEvent2WebhookDto(webhook))!; + await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(WebhookFactory.BuildEntityKey2WebhookDto(webhook))!; + await _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(WebhookFactory.BuildHeaders2WebhookDtos(webhook))!; + + return webhook; + } + + public async Task GetAsync(Guid key) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select() + .From() + .Where(x => x.Key == key); + + WebhookDto? webhookDto = await _scopeAccessor.AmbientScope?.Database.FirstOrDefaultAsync(sql)!; + + return webhookDto is null ? null : await DtoToEntity(webhookDto); + } + + public async Task> GetByEventNameAsync(string eventName) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .SelectAll() + .From() + .InnerJoin() + .On(left => left.Id, right => right.WebhookId) + .Where(x => x.Event == eventName); + + List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!; + + return new PagedModel + { + Items = await DtosToEntities(webhookDtos), + Total = webhookDtos.Count, + }; + } + + public async Task DeleteAsync(Webhook webhook) + { + Sql sql = _scopeAccessor.AmbientScope!.Database.SqlContext.Sql() + .Delete() + .Where(x => x.Key == webhook.Key); + + await _scopeAccessor.AmbientScope?.Database.ExecuteAsync(sql)!; + } + + public async Task UpdateAsync(Webhook webhook) + { + WebhookDto dto = WebhookFactory.BuildDto(webhook); + await _scopeAccessor.AmbientScope?.Database.UpdateAsync(dto)!; + + // Delete and re-insert the many to one references (event & entity keys) + DeleteManyToOneReferences(dto.Id); + InsertManyToOneReferences(webhook); + } + + private void DeleteManyToOneReferences(int webhookId) + { + _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); + _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); + _scopeAccessor.AmbientScope?.Database.Delete("WHERE webhookId = @webhookId", new { webhookId }); + } + + private void InsertManyToOneReferences(Webhook webhook) + { + IEnumerable buildEntityKey2WebhookDtos = WebhookFactory.BuildEntityKey2WebhookDto(webhook); + IEnumerable buildEvent2WebhookDtos = WebhookFactory.BuildEvent2WebhookDto(webhook); + IEnumerable header2WebhookDtos = WebhookFactory.BuildHeaders2WebhookDtos(webhook); + + _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(buildEntityKey2WebhookDtos); + _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(buildEvent2WebhookDtos); + _scopeAccessor.AmbientScope?.Database.InsertBulkAsync(header2WebhookDtos); + } + + private async Task> DtosToEntities(IEnumerable dtos) + { + List result = new(); + + foreach (WebhookDto webhook in dtos) + { + result.Add(await DtoToEntity(webhook)); + } + + return result; + } + + private async Task DtoToEntity(WebhookDto dto) + { + List? webhookEntityKeyDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync("WHERE webhookId = @webhookId", new { webhookId = dto.Id })!; + List? event2WebhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync("WHERE webhookId = @webhookId", new { webhookId = dto.Id })!; + List? headersWebhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync("WHERE webhookId = @webhookId", new { webhookId = dto.Id })!; + Webhook entity = WebhookFactory.BuildEntity(dto, webhookEntityKeyDtos, event2WebhookDtos, headersWebhookDtos); + + return entity; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index a71ccf5bed..d1a2c1b0d2 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -14,6 +14,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; /// public interface ISqlSyntaxProvider { + string Length { get; } + + string Substring { get; } + string ProviderName { get; } string CreateTable { get; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 15cb68bfc5..992896901e 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -421,6 +421,9 @@ public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider public virtual string DeleteDefaultConstraint => throw new NotSupportedException("Default constraints are not supported"); + public virtual string Length => "LEN"; + public virtual string Substring => "SUBSTRING"; + public virtual string CreateTable => "CREATE TABLE {0} ({1})"; public virtual string DropTable => "DROP TABLE {0}"; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 7b7199cc6e..bcb021b63e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -12,15 +12,9 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Core.PropertyEditors; -internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags - where TValue : BlockValue, new() - where TLayout : class, IBlockLayoutItem, new() +internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValueEditorBase { - private BlockEditorValues? _blockEditorValues; - private readonly IDataTypeService _dataTypeService; - private readonly ILogger> _logger; - private readonly PropertyEditorCollection _propertyEditors; - private readonly IJsonSerializer _jsonSerializer; + private BlockEditorValues? _blockEditorValues; protected BlockEditorPropertyValueEditor( DataEditorAttribute attribute, @@ -31,12 +25,8 @@ internal abstract class BlockEditorPropertyValueEditor : DataVa IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper) - : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) + : base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _logger = logger; - _jsonSerializer = jsonSerializer; } protected BlockEditorValues BlockEditorValues @@ -45,7 +35,8 @@ internal abstract class BlockEditorPropertyValueEditor : DataVa set => _blockEditorValues = value; } - public IEnumerable GetReferences(object? value) + /// + public override IEnumerable GetReferences(object? value) { var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); @@ -56,32 +47,11 @@ internal abstract class BlockEditorPropertyValueEditor : DataVa return Enumerable.Empty(); } - // loop through all content and settings data - foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) - { - foreach (KeyValuePair prop in row.PropertyValues) - { - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - - IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) - { - continue; - } - - var val = prop.Value.Value?.ToString(); - - IEnumerable refs = reference.GetReferences(val); - - result.AddRange(refs); - } - } - - return result; + return GetBlockValueReferences(blockEditorData.BlockValue); } /// - public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) + public override IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) { var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); @@ -91,31 +61,9 @@ internal abstract class BlockEditorPropertyValueEditor : DataVa return Enumerable.Empty(); } - var result = new List(); - // loop through all content and settings data - foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) - { - foreach (KeyValuePair prop in row.PropertyValues) - { - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - - IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); - if (valueEditor is not IDataValueTags tagsProvider) - { - continue; - } - - object? configurationObject = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.ConfigurationObject; - - result.AddRange(tagsProvider.GetTags(prop.Value.Value, configurationObject, languageId)); - } - } - - return result; + return GetBlockValueTags(blockEditorData.BlockValue, languageId); } - #region Convert database // editor - // note: there is NO variant support here /// @@ -145,8 +93,7 @@ internal abstract class BlockEditorPropertyValueEditor : DataVa return string.Empty; } - MapBlockItemDataToEditor(property, blockEditorData.BlockValue.ContentData); - MapBlockItemDataToEditor(property, blockEditorData.BlockValue.SettingsData); + MapBlockValueToEditor(property, blockEditorData.BlockValue); // return json convertable object return blockEditorData.BlockValue; @@ -181,93 +128,9 @@ internal abstract class BlockEditorPropertyValueEditor : DataVa return string.Empty; } - MapBlockItemDataFromEditor(blockEditorData.BlockValue.ContentData); - MapBlockItemDataFromEditor(blockEditorData.BlockValue.SettingsData); + MapBlockValueFromEditor(blockEditorData.BlockValue); // return json return _jsonSerializer.Serialize(blockEditorData.BlockValue); } - - private void MapBlockItemDataToEditor(IProperty property, List items) - { - var valEditors = new Dictionary(); - - foreach (BlockItemData row in items) - { - foreach (KeyValuePair prop in row.PropertyValues) - { - // create a temp property with the value - // - force it to be culture invariant as the block editor can't handle culture variant element properties - prop.Value.PropertyType.Variations = ContentVariation.Nothing; - var tempProp = new Property(prop.Value.PropertyType); - tempProp.SetValue(prop.Value.Value); - - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) - { - // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. - // if the property editor doesn't exist I think everything will break anyways? - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); - continue; - } - - IDataType? dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); - if (dataType == null) - { - // deal with weird situations by ignoring them (no comment) - row.PropertyValues.Remove(prop.Key); - _logger.LogWarning( - "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", - prop.Key, - row.Key, - property.PropertyType.Alias); - continue; - } - - if (!valEditors.TryGetValue(dataType.Id, out IDataValueEditor? valEditor)) - { - var tempConfig = dataType.ConfigurationObject; - valEditor = propEditor.GetValueEditor(tempConfig); - - valEditors.Add(dataType.Id, valEditor); - } - - var convValue = valEditor.ToEditor(tempProp); - - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = convValue; - } - } - } - - private void MapBlockItemDataFromEditor(List items) - { - foreach (BlockItemData row in items) - { - foreach (KeyValuePair prop in row.PropertyValues) - { - // Fetch the property types prevalue - var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId)?.ConfigurationObject; - - // Lookup the property editor - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) - { - continue; - } - - // Create a fake content property data object - var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); - - // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); - - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = newValue; - } - } - } - - #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs index beaf89fe9e..24b37045d4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs @@ -1,67 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.PropertyEditors; -internal class BlockEditorValidator : ComplexEditorValidator - where TValue : BlockValue, new() - where TLayout : class, IBlockLayoutItem, new() +internal class BlockEditorValidator : BlockEditorValidatorBase { private readonly BlockEditorValues _blockEditorValues; - private readonly IContentTypeService _contentTypeService; public BlockEditorValidator( IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, IContentTypeService contentTypeService) - : base(propertyValidationService) - { - _blockEditorValues = blockEditorValues; - _contentTypeService = contentTypeService; - } + : base(propertyValidationService, contentTypeService) + => _blockEditorValues = blockEditorValues; protected override IEnumerable GetElementTypeValidation(object? value) { BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value); - if (blockEditorData != null) - { - // There is no guarantee that the client will post data for every property defined in the Element Type but we still - // need to validate that data for each property especially for things like 'required' data to work. - // Lookup all element types for all content/settings and then we can populate any empty properties. - var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList(); - var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); - - foreach (BlockItemData row in allElements) - { - if (!allElementTypes.TryGetValue(row.ContentTypeKey, out IContentType? elementType)) - { - throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}"); - } - - // now ensure missing properties - foreach (IPropertyType elementTypeProp in elementType.CompositionPropertyTypes) - { - if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias)) - { - // set values to null - row.PropertyValues[elementTypeProp.Alias] = new BlockItemData.BlockPropertyValue(null, elementTypeProp); - row.RawPropertyValues[elementTypeProp.Alias] = null; - } - } - - var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); - foreach (KeyValuePair prop in row.PropertyValues) - { - elementValidation.AddPropertyTypeValidation( - new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); - } - - yield return elementValidation; - } - } + return blockEditorData is not null + ? GetBlockEditorDataValidation(blockEditorData) + : Array.Empty(); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs new file mode 100644 index 0000000000..977d235229 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -0,0 +1,51 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.PropertyEditors; + +internal abstract class BlockEditorValidatorBase : ComplexEditorValidator +{ + private readonly IContentTypeService _contentTypeService; + + protected BlockEditorValidatorBase(IPropertyValidationService propertyValidationService, IContentTypeService contentTypeService) + : base(propertyValidationService) + => _contentTypeService = contentTypeService; + + protected IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData) + { + // There is no guarantee that the client will post data for every property defined in the Element Type but we still + // need to validate that data for each property especially for things like 'required' data to work. + // Lookup all element types for all content/settings and then we can populate any empty properties. + var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList(); + var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); + + foreach (BlockItemData row in allElements) + { + if (!allElementTypes.TryGetValue(row.ContentTypeKey, out IContentType? elementType)) + { + throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}"); + } + + // now ensure missing properties + foreach (IPropertyType elementTypeProp in elementType.CompositionPropertyTypes) + { + if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias)) + { + // set values to null + row.PropertyValues[elementTypeProp.Alias] = new BlockItemData.BlockPropertyValue(null, elementTypeProp); + row.RawPropertyValues[elementTypeProp.Alias] = null; + } + } + + var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); + foreach (KeyValuePair prop in row.PropertyValues) + { + elementValidation.AddPropertyTypeValidation( + new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); + } + + yield return elementValidation; + } + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs index 48d063112b..2d2fcc5283 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs @@ -36,7 +36,17 @@ internal class BlockEditorValues } BlockEditorData blockEditorData = _dataConverter.Deserialize(propertyValueAsString); + return Clean(blockEditorData); + } + public BlockEditorData? ConvertAndClean(BlockValue blockValue) + { + BlockEditorData blockEditorData = _dataConverter.Convert(blockValue); + return Clean(blockEditorData); + } + + private BlockEditorData? Clean(BlockEditorData blockEditorData) + { if (blockEditorData.BlockValue.ContentData.Count == 0) { // if there's no content ensure there's no settings too diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs new file mode 100644 index 0000000000..d83d71abaa --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -0,0 +1,185 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Core.PropertyEditors; + +internal abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDataValueReference, IDataValueTags +{ + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ILogger _logger; + + protected BlockValuePropertyValueEditorBase( + DataEditorAttribute attribute, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + ILocalizedTextService textService, + ILogger logger, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper) + : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) + { + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _logger = logger; + } + + /// + public abstract IEnumerable GetReferences(object? value); + + protected IEnumerable GetBlockValueReferences(BlockValue blockValue) + { + var result = new List(); + + // loop through all content and settings data + foreach (BlockItemData row in blockValue.ContentData.Concat(blockValue.SettingsData)) + { + foreach (KeyValuePair prop in row.PropertyValues) + { + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) + { + continue; + } + + var val = prop.Value.Value?.ToString(); + + IEnumerable refs = reference.GetReferences(val); + + result.AddRange(refs); + } + } + + return result; + } + + /// + public abstract IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId); + + protected IEnumerable GetBlockValueTags(BlockValue blockValue, int? languageId) + { + var result = new List(); + // loop through all content and settings data + foreach (BlockItemData row in blockValue.ContentData.Concat(blockValue.SettingsData)) + { + foreach (KeyValuePair prop in row.PropertyValues) + { + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (valueEditor is not IDataValueTags tagsProvider) + { + continue; + } + + object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration; + + result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); + } + } + + return result; + } + + protected void MapBlockValueFromEditor(BlockValue blockValue) + { + MapBlockItemDataFromEditor(blockValue.ContentData); + MapBlockItemDataFromEditor(blockValue.SettingsData); + } + + protected void MapBlockValueToEditor(IProperty property, BlockValue blockValue) + { + MapBlockItemDataToEditor(property, blockValue.ContentData); + MapBlockItemDataToEditor(property, blockValue.SettingsData); + } + + private void MapBlockItemDataToEditor(IProperty property, List items) + { + var valEditors = new Dictionary(); + + foreach (BlockItemData row in items) + { + foreach (KeyValuePair prop in row.PropertyValues) + { + // create a temp property with the value + // - force it to be culture invariant as the block editor can't handle culture variant element properties + prop.Value.PropertyType.Variations = ContentVariation.Nothing; + var tempProp = new Property(prop.Value.PropertyType); + tempProp.SetValue(prop.Value.Value); + + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) + { + // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. + // if the property editor doesn't exist I think everything will break anyways? + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + continue; + } + + IDataType? dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); + if (dataType == null) + { + // deal with weird situations by ignoring them (no comment) + row.PropertyValues.Remove(prop.Key); + _logger.LogWarning( + "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", + prop.Key, + row.Key, + property.PropertyType.Alias); + continue; + } + + if (!valEditors.TryGetValue(dataType.Id, out IDataValueEditor? valEditor)) + { + var tempConfig = dataType.Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataType.Id, valEditor); + } + + var convValue = valEditor.ToEditor(tempProp); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = convValue; + } + } + } + + private void MapBlockItemDataFromEditor(List items) + { + foreach (BlockItemData row in items) + { + foreach (KeyValuePair prop in row.PropertyValues) + { + // Fetch the property types prevalue + var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId)?.Configuration; + + // Lookup the property editor + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) + { + continue; + } + + // Create a fake content property data object + var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); + + // Get the property editor to do it's conversion + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = newValue; + } + } + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 530e69ff05..1105b76ac0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -28,7 +28,7 @@ namespace Umbraco.Cms.Core.PropertyEditors; Icon = "icon-thumbnail-list", ValueEditorIsReusable = false, IsDeprecated = true)] -[Obsolete("Nested content is obsolete, will be removed in V13")] +[Obsolete("Nested content is obsolete, will be removed in Umbraco 14")] public class NestedContentPropertyEditor : DataEditor { public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; @@ -36,7 +36,7 @@ public class NestedContentPropertyEditor : DataEditor private readonly IIOHelper _ioHelper; private readonly INestedContentPropertyIndexValueFactory _nestedContentPropertyIndexValueFactory; - [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 12.")] + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 14.")] public NestedContentPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper) @@ -44,7 +44,7 @@ public class NestedContentPropertyEditor : DataEditor { } - [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 13.")] + [Obsolete("Use non-obsoleted ctor. This will be removed in Umbraco 14.")] public NestedContentPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper, diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs new file mode 100644 index 0000000000..01d10e46f0 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class RichTextEditorBlockValidator : BlockEditorValidatorBase +{ + private readonly BlockEditorValues _blockEditorValues; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + + public RichTextEditorBlockValidator( + IPropertyValidationService propertyValidationService, + BlockEditorValues blockEditorValues, + IContentTypeService contentTypeService, + IJsonSerializer jsonSerializer, + ILogger logger) + : base(propertyValidationService, contentTypeService) + { + _blockEditorValues = blockEditorValues; + _jsonSerializer = jsonSerializer; + _logger = logger; + } + + protected override IEnumerable GetElementTypeValidation(object? value) + { + RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue); + if (richTextEditorValue?.Blocks is null) + { + return Array.Empty(); + } + + BlockEditorData? blockEditorData = _blockEditorValues.ConvertAndClean(richTextEditorValue.Blocks); + return blockEditorData is not null + ? GetBlockEditorDataValidation(blockEditorData) + : Array.Empty(); + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 85bf390e9b..63a9cfa304 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -1,18 +1,20 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; -using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.Macros; using Umbraco.Cms.Infrastructure.Templates; using Umbraco.Extensions; @@ -33,16 +35,11 @@ namespace Umbraco.Cms.Core.PropertyEditors; ValueEditorIsReusable = true)] public class RichTextPropertyEditor : DataEditor { - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IEditorConfigurationParser _editorConfigurationParser; - private readonly HtmlImageSourceParser _imageSourceParser; - private readonly IImageUrlGenerator _imageUrlGenerator; private readonly IIOHelper _ioHelper; - private readonly HtmlLocalLinkParser _localLinkParser; - private readonly IHtmlMacroParameterParser _macroParameterParser; - private readonly RichTextEditorPastedImages _pastedImages; + private readonly IRichTextPropertyIndexValueFactory _richTextPropertyIndexValueFactory; - [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")] + [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead. Will be removed in V15.")] public RichTextPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, @@ -65,7 +62,7 @@ public class RichTextPropertyEditor : DataEditor { } - [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")] + [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead. Will be removed in V15.")] public RichTextPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, @@ -87,9 +84,7 @@ public class RichTextPropertyEditor : DataEditor { } - /// - /// The constructor will setup the property editor based on the attribute if one is found. - /// + [Obsolete($"Use the constructor which accepts an {nameof(IRichTextPropertyIndexValueFactory)} parameter. Will be removed in V15.")] public RichTextPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, @@ -100,20 +95,57 @@ public class RichTextPropertyEditor : DataEditor IImageUrlGenerator imageUrlGenerator, IHtmlMacroParameterParser macroParameterParser, IEditorConfigurationParser editorConfigurationParser) + : this( + dataValueEditorFactory, + backOfficeSecurityAccessor, + imageSourceParser, + localLinkParser, + pastedImages, + ioHelper, + imageUrlGenerator, + macroParameterParser, + editorConfigurationParser, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete($"Use the non-obsolete constructor. Will be removed in V15.")] + public RichTextPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + HtmlImageSourceParser imageSourceParser, + HtmlLocalLinkParser localLinkParser, + RichTextEditorPastedImages pastedImages, + IIOHelper ioHelper, + IImageUrlGenerator imageUrlGenerator, + IHtmlMacroParameterParser macroParameterParser, + IEditorConfigurationParser editorConfigurationParser, + IRichTextPropertyIndexValueFactory richTextPropertyIndexValueFactory) + : this( + dataValueEditorFactory, + editorConfigurationParser, + ioHelper, + richTextPropertyIndexValueFactory) + { + } + + /// + /// The constructor will setup the property editor based on the attribute if one is found. + /// + public RichTextPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IEditorConfigurationParser editorConfigurationParser, + IIOHelper ioHelper, + IRichTextPropertyIndexValueFactory richTextPropertyIndexValueFactory) : base(dataValueEditorFactory) { - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _imageSourceParser = imageSourceParser; - _localLinkParser = localLinkParser; - _pastedImages = pastedImages; _ioHelper = ioHelper; - _imageUrlGenerator = imageUrlGenerator; - _macroParameterParser = macroParameterParser; + _richTextPropertyIndexValueFactory = richTextPropertyIndexValueFactory; _editorConfigurationParser = editorConfigurationParser; SupportsReadOnly = true; } - public override IPropertyIndexValueFactory PropertyIndexValueFactory => new RichTextPropertyIndexValueFactory(); + public override IPropertyIndexValueFactory PropertyIndexValueFactory => _richTextPropertyIndexValueFactory; /// /// Create a custom value editor @@ -129,67 +161,48 @@ public class RichTextPropertyEditor : DataEditor /// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for /// display in the editor /// - internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference + internal class RichTextPropertyValueEditor : BlockValuePropertyValueEditorBase { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IHtmlSanitizer _htmlSanitizer; private readonly HtmlImageSourceParser _imageSourceParser; - private readonly IImageUrlGenerator _imageUrlGenerator; private readonly HtmlLocalLinkParser _localLinkParser; private readonly IHtmlMacroParameterParser _macroParameterParser; private readonly RichTextEditorPastedImages _pastedImages; + private readonly IJsonSerializer _jsonSerializer; + private readonly IContentTypeService _contentTypeService; + private readonly ILogger _logger; public RichTextPropertyValueEditor( DataEditorAttribute attribute, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + ILogger logger, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, - IImageUrlGenerator imageUrlGenerator, IJsonSerializer jsonSerializer, IIOHelper ioHelper, IHtmlSanitizer htmlSanitizer, - IHtmlMacroParameterParser macroParameterParser) - : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + IHtmlMacroParameterParser macroParameterParser, + IContentTypeService contentTypeService, + IPropertyValidationService propertyValidationService) + : base(attribute, propertyEditors, dataTypeService, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; _pastedImages = pastedImages; - _imageUrlGenerator = imageUrlGenerator; _htmlSanitizer = htmlSanitizer; _macroParameterParser = macroParameterParser; - } + _contentTypeService = contentTypeService; + _jsonSerializer = jsonSerializer; + _logger = logger; - [Obsolete("Use the constructor which takes an HtmlMacroParameterParser instead")] - public RichTextPropertyValueEditor( - DataEditorAttribute attribute, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - HtmlImageSourceParser imageSourceParser, - HtmlLocalLinkParser localLinkParser, - RichTextEditorPastedImages pastedImages, - IImageUrlGenerator imageUrlGenerator, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - IHtmlSanitizer htmlSanitizer) - : this( - attribute, - backOfficeSecurityAccessor, - localizedTextService, - shortStringHelper, - imageSourceParser, - localLinkParser, - pastedImages, - imageUrlGenerator, - jsonSerializer, - ioHelper, - htmlSanitizer, - StaticServiceProvider.Instance.GetRequiredService()) - { + Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), contentTypeService, jsonSerializer, logger)); } /// @@ -221,30 +234,57 @@ public class RichTextPropertyEditor : DataEditor /// /// /// - public IEnumerable GetReferences(object? value) + public override IEnumerable GetReferences(object? value) { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString()!; - - foreach (Udi udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) + if (TryParseEditorValue(value, out RichTextEditorValue? richTextEditorValue) is false) { - yield return new UmbracoEntityReference(udi); + return Array.Empty(); } - foreach (Udi? udi in _localLinkParser.FindUdisFromLocalLinks(asString)) - { - if (udi is not null) - { - yield return new UmbracoEntityReference(udi); - } - } + var references = new List(); + + // image references from markup + references.AddRange(_imageSourceParser + .FindUdisFromDataAttributes(richTextEditorValue.Markup) + .Select(udi => new UmbracoEntityReference(udi))); + + // local link references from markup + references.AddRange(_localLinkParser + .FindUdisFromLocalLinks(richTextEditorValue.Markup) + .WhereNotNull() + .Select(udi => new UmbracoEntityReference(udi))); // TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs // UPDATE: We are getting the Macros in 'FindUmbracoEntityReferencesFromEmbeddedMacros' - perhaps we just return the macro Udis here too or do they need their own relationAlias? - foreach (UmbracoEntityReference umbracoEntityReference in _macroParameterParser - .FindUmbracoEntityReferencesFromEmbeddedMacros(asString)) + references.AddRange(_macroParameterParser.FindUmbracoEntityReferencesFromEmbeddedMacros(richTextEditorValue.Markup)); + + // references from blocks + if (richTextEditorValue.Blocks is not null) { - yield return umbracoEntityReference; + BlockEditorData? blockEditorData = ConvertAndClean(richTextEditorValue.Blocks); + if (blockEditorData is not null) + { + references.AddRange(GetBlockValueReferences(blockEditorData.BlockValue)); + } } + + return references; + } + + public override IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) + { + if (TryParseEditorValue(value, out RichTextEditorValue? richTextEditorValue) is false || richTextEditorValue.Blocks is null) + { + return Array.Empty(); + } + + BlockEditorData? blockEditorData = ConvertAndClean(richTextEditorValue.Blocks); + if (blockEditorData is null) + { + return Array.Empty(); + } + + return GetBlockValueTags(blockEditorData.BlockValue, languageId); } /// @@ -255,17 +295,20 @@ public class RichTextPropertyEditor : DataEditor /// public override object? ToEditor(IProperty property, string? culture = null, string? segment = null) { - var val = property.GetValue(culture, segment); - if (val == null) + var value = property.GetValue(culture, segment); + if (TryParseEditorValue(value, out RichTextEditorValue? richTextEditorValue) is false) { return null; } - var propertyValueWithMediaResolved = _imageSourceParser.EnsureImageSources(val.ToString()!); + var propertyValueWithMediaResolved = _imageSourceParser.EnsureImageSources(richTextEditorValue.Markup); var parsed = MacroTagParser.FormatRichTextPersistedDataForEditor( propertyValueWithMediaResolved, new Dictionary()); - return parsed; + richTextEditorValue.Markup = parsed; + + // return json convertable object + return CleanAndMapBlocks(richTextEditorValue, blockValue => MapBlockValueToEditor(property, blockValue)); } /// @@ -276,7 +319,7 @@ public class RichTextPropertyEditor : DataEditor /// public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) { - if (editorValue.Value == null) + if (TryParseEditorValue(editorValue.Value, out RichTextEditorValue? richTextEditorValue) is false) { return null; } @@ -288,46 +331,67 @@ public class RichTextPropertyEditor : DataEditor GuidUdi? mediaParent = config?.MediaParentId; Guid mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; - if (string.IsNullOrWhiteSpace(editorValue.Value.ToString())) + if (string.IsNullOrWhiteSpace(richTextEditorValue.Markup)) { return null; } + var parseAndSaveBase64Images = _pastedImages.FindAndPersistEmbeddedImages( + richTextEditorValue.Markup, mediaParentId, userKey); var parseAndSavedTempImages = _pastedImages - .FindAndPersistPastedTempImagesAsync(editorValue.Value.ToString()!, mediaParentId, userKey, _imageUrlGenerator) + .FindAndPersistPastedTempImagesAsync(parseAndSaveBase64Images, mediaParentId, userKey, _imageUrlGenerator) .GetAwaiter() .GetResult(); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); var sanitized = _htmlSanitizer.Sanitize(parsed); - return sanitized.NullOrWhiteSpaceAsNull(); + richTextEditorValue.Markup = sanitized.NullOrWhiteSpaceAsNull() ?? string.Empty; + + RichTextEditorValue cleanedUpRichTextEditorValue = CleanAndMapBlocks(richTextEditorValue, MapBlockValueFromEditor); + + // return json + return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(cleanedUpRichTextEditorValue, _jsonSerializer); } - } - internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory - { - public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable availableCultures) + private bool TryParseEditorValue(object? value, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue) + => RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out richTextEditorValue); + + private RichTextEditorValue CleanAndMapBlocks(RichTextEditorValue richTextEditorValue, Action handleMapping) { - var val = property.GetValue(culture, segment, published); - - if (!(val is string strVal)) + if (richTextEditorValue.Blocks is null) { - yield break; + // no blocks defined, store empty block value + return MarkupWithEmptyBlocks(); } - // index the stripped HTML values - yield return new KeyValuePair>( - property.Alias, - new object[] { strVal.StripHtml() }); + BlockEditorData? blockEditorData = ConvertAndClean(richTextEditorValue.Blocks); - // store the raw value - yield return new KeyValuePair>( - $"{UmbracoExamineFieldNames.RawFieldPrefix}{property.Alias}", new object[] { strVal }); + if (blockEditorData is not null) + { + handleMapping(blockEditorData.BlockValue); + return new RichTextEditorValue + { + Markup = richTextEditorValue.Markup, Blocks = blockEditorData.BlockValue + }; + } + + // could not deserialize the blocks or handle the mapping, store empty block value + return MarkupWithEmptyBlocks(); + + RichTextEditorValue MarkupWithEmptyBlocks() => new() + { + Markup = richTextEditorValue.Markup, Blocks = new BlockValue() + }; } - [Obsolete("Use the overload with the 'availableCultures' parameter instead, scheduled for removal in v14")] - public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) - => GetIndexValues(property, culture, segment, published, Enumerable.Empty()); + private BlockEditorData? ConvertAndClean(BlockValue blockValue) + { + BlockEditorValues blockEditorValues = CreateBlockEditorValues(); + return blockEditorValues.ConvertAndClean(blockValue); + } + + private BlockEditorValues CreateBlockEditorValues() + => new(new RichTextEditorBlockDataConverter(), _contentTypeService, _logger); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorHelper.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorHelper.cs new file mode 100644 index 0000000000..72f7d10dc5 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorHelper.cs @@ -0,0 +1,62 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors; + +// NOTE: this class is deliberately made accessible to 3rd party consumers (i.e. Deploy, uSync, ...) +public static class RichTextPropertyEditorHelper +{ + /// + /// Attempts to parse a instance from a property value. + /// + /// The property value. + /// The system JSON serializer. + /// A logger for error message handling. + /// The parsed instance, or null if parsing fails. + /// True if the parsing succeeds, false otherwise + /// + /// The passed value can be: + /// - a JSON string. + /// - a JSON object. + /// - a raw markup string (for backwards compatability). + /// + public static bool TryParseRichTextEditorValue(object? value, IJsonSerializer jsonSerializer, ILogger logger, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue) + { + var stringValue = value as string ?? value?.ToString(); + if (stringValue is null) + { + richTextEditorValue = null; + return false; + } + + if (stringValue.DetectIsJson() is false) + { + // assume value is raw markup and construct the model accordingly (no blocks stored) + richTextEditorValue = new RichTextEditorValue { Markup = stringValue, Blocks = null }; + return true; + } + + try + { + richTextEditorValue = jsonSerializer.Deserialize(stringValue); + return richTextEditorValue != null; + } + catch (Exception exception) + { + logger.LogError(exception, "Could not parse rich text editor value, see exception for details."); + richTextEditorValue = null; + return false; + } + } + + /// + /// Serializes a instance for property value storage. + /// + /// The instance to serialize. + /// The system JSON serializer. + /// A string value representing the passed instance. + public static string SerializeRichTextEditorValue(RichTextEditorValue richTextEditorValue, IJsonSerializer jsonSerializer) + => jsonSerializer.Serialize(richTextEditorValue); +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs new file mode 100644 index 0000000000..be49e280cb --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class RichTextPropertyIndexValueFactory : NestedPropertyIndexValueFactoryBase, IRichTextPropertyIndexValueFactory +{ + private readonly IJsonSerializer _jsonSerializer; + private readonly IContentTypeService _contentTypeService; + private readonly ILogger _logger; + + public RichTextPropertyIndexValueFactory( + PropertyEditorCollection propertyEditorCollection, + IJsonSerializer jsonSerializer, + IOptionsMonitor indexingSettings, + IContentTypeService contentTypeService, + ILogger logger) + : base(propertyEditorCollection, jsonSerializer, indexingSettings) + { + _jsonSerializer = jsonSerializer; + _contentTypeService = contentTypeService; + _logger = logger; + } + + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable availableCultures) + { + var val = property.GetValue(culture, segment, published); + if (RichTextPropertyEditorHelper.TryParseRichTextEditorValue(val, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue) is false) + { + yield break; + } + + // the "blocks values resume" (the combined searchable text values from all blocks) is stored as a string value under the property alias by the base implementation + var blocksIndexValues = base.GetIndexValues(property, culture, segment, published, availableCultures).ToDictionary(pair => pair.Key, pair => pair.Value); + var blocksIndexValuesResume = blocksIndexValues.TryGetValue(property.Alias, out IEnumerable? blocksIndexValuesResumeValue) + ? blocksIndexValuesResumeValue.FirstOrDefault() as string + : null; + + // index the stripped HTML values combined with "blocks values resume" value + yield return new KeyValuePair>( + property.Alias, + new object[] { $"{richTextEditorValue.Markup.StripHtml()} {blocksIndexValuesResume}" }); + + // store the raw value + yield return new KeyValuePair>( + $"{UmbracoExamineFieldNames.RawFieldPrefix}{property.Alias}", new object[] { richTextEditorValue.Markup }); + } + + [Obsolete("Use the overload with the 'availableCultures' parameter instead, scheduled for removal in v14")] + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + => GetIndexValues(property, culture, segment, published, Enumerable.Empty()); + + protected override IContentType? GetContentTypeOfNestedItem(BlockItemData nestedItem) + => _contentTypeService.Get(nestedItem.ContentTypeKey); + + protected override IDictionary GetRawProperty(BlockItemData blockItemData) + => blockItemData.RawPropertyValues; + + protected override IEnumerable GetDataItems(RichTextEditorValue input) + => input.Blocks?.ContentData ?? new List(); +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs index 7270064840..5b877bd9b9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs @@ -11,14 +11,14 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; -using static Umbraco.Cms.Core.PropertyEditors.BlockGridConfiguration; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { [DefaultPropertyValueConverter(typeof(JsonValueConverter))] - public class BlockGridPropertyValueConverter : BlockPropertyValueConverterBase, IDeliveryApiPropertyValueConverter + public class BlockGridPropertyValueConverter : PropertyValueConverterBase, IDeliveryApiPropertyValueConverter { private readonly IProfilingLogger _proflog; + private readonly BlockEditorConverter _blockConverter; private readonly IJsonSerializer _jsonSerializer; private readonly IApiElementBuilder _apiElementBuilder; @@ -32,15 +32,14 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters } - // Niels, Change: I would love if this could be general, so we don't need a specific one for each block property editor.... public BlockGridPropertyValueConverter( IProfilingLogger proflog, BlockEditorConverter blockConverter, IJsonSerializer jsonSerializer, IApiElementBuilder apiElementBuilder) - : base(blockConverter) { _proflog = proflog; + _blockConverter = blockConverter; _jsonSerializer = jsonSerializer; _apiElementBuilder = apiElementBuilder; } @@ -49,14 +48,22 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockGrid); + /// + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(BlockGridModel); + + /// public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => ConvertIntermediateToBlockGridModel(propertyType, referenceCacheLevel, inter, preview); + /// public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType); + /// public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(ApiBlockGridModel); + /// public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { const int defaultColumns = 12; @@ -96,6 +103,18 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { using (!_proflog.IsEnabled(LogLevel.Debug) ? null : _proflog.DebugDuration($"ConvertPropertyToBlockGrid ({propertyType.DataType.Id})")) { + // NOTE: this is to retain backwards compatability + if (inter is null) + { + return BlockGridModel.Empty; + } + + // NOTE: The intermediate object is just a JSON string, we don't actually convert from source -> intermediate since source is always just a JSON string + if (inter is not string intermediateBlockModelValue) + { + return null; + } + // Get configuration BlockGridConfiguration? configuration = propertyType.DataType.ConfigurationAs(); if (configuration is null) @@ -103,58 +122,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters return null; } - BlockGridModel CreateEmptyModel() => BlockGridModel.Empty; - - BlockGridModel CreateModel(IList items) => new BlockGridModel(items, configuration.GridColumns); - - BlockGridItem? EnrichBlockItem(BlockGridItem blockItem, BlockGridLayoutItem layoutItem, BlockGridBlockConfiguration blockConfig, CreateBlockItemModelFromLayout createBlockItem) - { - // enrich block item with additional configs + setup areas - var blockConfigAreaMap = blockConfig.Areas.ToDictionary(area => area.Key); - - blockItem.RowSpan = layoutItem.RowSpan!.Value; - blockItem.ColumnSpan = layoutItem.ColumnSpan!.Value; - blockItem.AreaGridColumns = blockConfig.AreaGridColumns; - blockItem.GridColumns = configuration.GridColumns; - blockItem.Areas = layoutItem.Areas.Select(area => - { - if (!blockConfigAreaMap.TryGetValue(area.Key, out BlockGridAreaConfiguration? areaConfig)) - { - return null; - } - - var items = area.Items.Select(item => createBlockItem(item)).WhereNotNull().ToList(); - return new BlockGridArea(items, areaConfig.Alias!, areaConfig.RowSpan!.Value, areaConfig.ColumnSpan!.Value); - }).WhereNotNull().ToArray(); - - return blockItem; - } - - BlockGridModel blockModel = UnwrapBlockModel( - referenceCacheLevel, - inter, - preview, - configuration.Blocks, - CreateEmptyModel, - CreateModel, - EnrichBlockItem - ); - - return blockModel; + var creator = new BlockGridPropertyValueCreator(_blockConverter, _jsonSerializer); + return creator.CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks, configuration.GridColumns); } } - - protected override BlockGridEditorDataConverter CreateBlockEditorDataConverter() => new(_jsonSerializer); - - protected override BlockItemActivator CreateBlockItemActivator() => new BlockGridItemActivator(BlockEditorConverter); - - private class BlockGridItemActivator : BlockItemActivator - { - public BlockGridItemActivator(BlockEditorConverter blockConverter) : base(blockConverter) - { - } - - protected override Type GenericItemType => typeof(BlockGridItem<,>); - } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs new file mode 100644 index 0000000000..b50d95f5c3 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs @@ -0,0 +1,68 @@ +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +internal class BlockGridPropertyValueCreator : BlockPropertyValueCreatorBase +{ + private readonly IJsonSerializer _jsonSerializer; + + public BlockGridPropertyValueCreator(BlockEditorConverter blockEditorConverter, IJsonSerializer jsonSerializer) + : base(blockEditorConverter) + => _jsonSerializer = jsonSerializer; + + public BlockGridModel CreateBlockModel(PropertyCacheLevel referenceCacheLevel, string intermediateBlockModelValue, bool preview, BlockGridConfiguration.BlockGridBlockConfiguration[] blockConfigurations, int? gridColumns) + { + BlockGridModel CreateEmptyModel() => BlockGridModel.Empty; + + BlockGridModel CreateModel(IList items) => new BlockGridModel(items, gridColumns); + + BlockGridItem? EnrichBlockItem(BlockGridItem blockItem, BlockGridLayoutItem layoutItem, BlockGridConfiguration.BlockGridBlockConfiguration blockConfig, CreateBlockItemModelFromLayout createBlockItem) + { + // enrich block item with additional configs + setup areas + var blockConfigAreaMap = blockConfig.Areas.ToDictionary(area => area.Key); + + blockItem.RowSpan = layoutItem.RowSpan!.Value; + blockItem.ColumnSpan = layoutItem.ColumnSpan!.Value; + blockItem.AreaGridColumns = blockConfig.AreaGridColumns; + blockItem.GridColumns = gridColumns; + blockItem.Areas = layoutItem.Areas.Select(area => + { + if (!blockConfigAreaMap.TryGetValue(area.Key, out BlockGridConfiguration.BlockGridAreaConfiguration? areaConfig)) + { + return null; + } + + var items = area.Items.Select(item => createBlockItem(item)).WhereNotNull().ToList(); + return new BlockGridArea(items, areaConfig.Alias!, areaConfig.RowSpan!.Value, areaConfig.ColumnSpan!.Value); + }).WhereNotNull().ToArray(); + + return blockItem; + } + + BlockGridModel blockModel = CreateBlockModel( + referenceCacheLevel, + intermediateBlockModelValue, + preview, + blockConfigurations, + CreateEmptyModel, + CreateModel, + EnrichBlockItem); + + return blockModel; + } + + protected override BlockEditorDataConverter CreateBlockEditorDataConverter() => new BlockGridEditorDataConverter(_jsonSerializer); + + protected override BlockItemActivator CreateBlockItemActivator() => new BlockGridItemActivator(BlockEditorConverter); + + private class BlockGridItemActivator : BlockItemActivator + { + public BlockGridItemActivator(BlockEditorConverter blockConverter) : base(blockConverter) + { + } + + protected override Type GenericItemType => typeof(BlockGridItem<,>); + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 99cefe1c29..f55c82d363 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -11,17 +11,18 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Extensions; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -using static Umbraco.Cms.Core.PropertyEditors.BlockListConfiguration; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; [DefaultPropertyValueConverter(typeof(JsonValueConverter))] -public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase, IDeliveryApiPropertyValueConverter +public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeliveryApiPropertyValueConverter { private readonly IContentTypeService _contentTypeService; private readonly IProfilingLogger _proflog; + private readonly BlockEditorConverter _blockConverter; private readonly IApiElementBuilder _apiElementBuilder; private readonly IJsonSerializer _jsonSerializer; @@ -44,9 +45,9 @@ public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase new ApiBlockItem( - _apiElementBuilder.Build(item.Content), - item.Settings != null ? _apiElementBuilder.Build(item.Settings) : null)) - .ToArray() + ? model.Select(item => item.CreateApiBlockItem(_apiElementBuilder)).ToArray() : Array.Empty()); } private BlockListModel? ConvertIntermediateToBlockListModel(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { - // NOTE: The intermediate object is just a JSON string, we don't actually convert from source -> intermediate since source is always just a JSON string using (!_proflog.IsEnabled(LogLevel.Debug) ? null : _proflog.DebugDuration( $"ConvertPropertyToBlockList ({propertyType.DataType.Id})")) { + // NOTE: this is to retain backwards compatability + if (inter is null) + { + return BlockListModel.Empty; + } + + // NOTE: The intermediate object is just a JSON string, we don't actually convert from source -> intermediate since source is always just a JSON string + if (inter is not string intermediateBlockModelValue) + { + return null; + } + // Get configuration BlockListConfiguration? configuration = propertyType.DataType.ConfigurationAs(); if (configuration is null) @@ -151,26 +159,8 @@ public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase BlockListModel.Empty; - - BlockListModel CreateModel(IList items) => new BlockListModel(items); - - BlockListModel blockModel = UnwrapBlockModel(referenceCacheLevel, inter, preview, configuration.Blocks, CreateEmptyModel, CreateModel); - - return blockModel; + var creator = new BlockListPropertyValueCreator(_blockConverter); + return creator.CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks); } } - - protected override BlockListEditorDataConverter CreateBlockEditorDataConverter() => new(_jsonSerializer); - - protected override BlockItemActivator CreateBlockItemActivator() => new BlockListItemActivator(BlockEditorConverter); - - private class BlockListItemActivator : BlockItemActivator - { - public BlockListItemActivator(BlockEditorConverter blockConverter) : base(blockConverter) - { - } - - protected override Type GenericItemType => typeof(BlockListItem<,>); - } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs new file mode 100644 index 0000000000..952dc43e2f --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs @@ -0,0 +1,35 @@ +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +internal class BlockListPropertyValueCreator : BlockPropertyValueCreatorBase +{ + public BlockListPropertyValueCreator(BlockEditorConverter blockEditorConverter) + : base(blockEditorConverter) + { + } + + public BlockListModel CreateBlockModel(PropertyCacheLevel referenceCacheLevel, string intermediateBlockModelValue, bool preview, BlockListConfiguration.BlockConfiguration[] blockConfigurations) + { + BlockListModel CreateEmptyModel() => BlockListModel.Empty; + + BlockListModel CreateModel(IList items) => new BlockListModel(items); + + BlockListModel blockModel = CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); + + return blockModel; + } + + protected override BlockEditorDataConverter CreateBlockEditorDataConverter() => new BlockListEditorDataConverter(); + + protected override BlockItemActivator CreateBlockItemActivator() => new BlockListItemActivator(BlockEditorConverter); + + private class BlockListItemActivator : BlockItemActivator + { + public BlockListItemActivator(BlockEditorConverter blockConverter) : base(blockConverter) + { + } + + protected override Type GenericItemType => typeof(BlockListItem<,>); + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs index 414cf16d22..dd0d206a78 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs @@ -8,6 +8,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; +[Obsolete("Please use implementations of BlockPropertyValueCreatorBase instead of this. See BlockListPropertyValueConverter for inspiration.. Will be removed in V15.")] public abstract class BlockPropertyValueConverterBase : PropertyValueConverterBase where TBlockItemModel : class, IBlockReference where TBlockLayoutItem : class, IBlockLayoutItem, new() diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs new file mode 100644 index 0000000000..84a82338db --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs @@ -0,0 +1,266 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Reflection; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +internal abstract class BlockPropertyValueCreatorBase + where TBlockModel : BlockModelCollection + where TBlockItemModel : class, IBlockReference + where TBlockLayoutItem : IBlockLayoutItem + where TBlockConfiguration : IBlockConfiguration +{ + /// + /// Creates a specific data converter for the block property implementation. + /// + /// + protected abstract BlockEditorDataConverter CreateBlockEditorDataConverter(); + + /// + /// Creates a specific block item activator for the block property implementation. + /// + /// + protected abstract BlockItemActivator CreateBlockItemActivator(); + + /// + /// Creates an empty block model, i.e. for uninitialized or invalid property values. + /// + /// + protected delegate TBlockModel CreateEmptyBlockModel(); + + /// + /// Creates a block model for a list of block items. + /// + /// The block items to base the block model on. + /// + protected delegate TBlockModel CreateBlockModelFromItems(IList blockItems); + + /// + /// Creates a block item from a block layout item. + /// + /// The block layout item to base the block item on. + /// + protected delegate TBlockItemModel? CreateBlockItemModelFromLayout(TBlockLayoutItem layoutItem); + + /// + /// Enriches a block item after it has been created by the block item activator. Use this to set block item data based on concrete block layout and configuration. + /// + /// The block item to enrich. + /// The block layout item for the block item being enriched. + /// The configuration of the block. + /// Delegate for creating new block items from block layout items. + /// + protected delegate TBlockItemModel? EnrichBlockItemModelFromConfiguration(TBlockItemModel item, TBlockLayoutItem layoutItem, TBlockConfiguration configuration, CreateBlockItemModelFromLayout blockItemModelCreator); + + protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter) => BlockEditorConverter = blockEditorConverter; + + protected BlockEditorConverter BlockEditorConverter { get; } + + protected TBlockModel CreateBlockModel( + PropertyCacheLevel referenceCacheLevel, + string intermediateBlockModelValue, + bool preview, + IEnumerable blockConfigurations, + CreateEmptyBlockModel createEmptyModel, + CreateBlockModelFromItems createModelFromItems, + EnrichBlockItemModelFromConfiguration? enrichBlockItem = null) + { + // Short-circuit on empty values + if (intermediateBlockModelValue.IsNullOrWhiteSpace()) + { + return createEmptyModel(); + } + + BlockEditorDataConverter blockEditorDataConverter = CreateBlockEditorDataConverter(); + BlockEditorData converted = blockEditorDataConverter.Deserialize(intermediateBlockModelValue); + return CreateBlockModel(referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); + } + + protected TBlockModel CreateBlockModel( + PropertyCacheLevel referenceCacheLevel, + BlockValue blockValue, + bool preview, + IEnumerable blockConfigurations, + CreateEmptyBlockModel createEmptyModel, + CreateBlockModelFromItems createModelFromItems, + EnrichBlockItemModelFromConfiguration? enrichBlockItem = null) + { + BlockEditorDataConverter blockEditorDataConverter = CreateBlockEditorDataConverter(); + BlockEditorData converted = blockEditorDataConverter.Convert(blockValue); + return CreateBlockModel(referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); + } + + private TBlockModel CreateBlockModel( + PropertyCacheLevel referenceCacheLevel, + BlockEditorData converted, + bool preview, + IEnumerable blockConfigurations, + CreateEmptyBlockModel createEmptyModel, + CreateBlockModelFromItems createModelFromItems, + EnrichBlockItemModelFromConfiguration? enrichBlockItem = null) + { + if (converted.BlockValue.ContentData.Count == 0) + { + return createEmptyModel(); + } + + IEnumerable? layout = converted.Layout?.ToObject>(); + if (layout is null) + { + return createEmptyModel(); + } + + var blockConfigMap = blockConfigurations.ToDictionary(bc => bc.ContentElementTypeKey); + + // Convert the content data + var contentPublishedElements = new Dictionary(); + foreach (BlockItemData data in converted.BlockValue.ContentData) + { + if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) + { + continue; + } + + IPublishedElement? element = BlockEditorConverter.ConvertToElement(data, referenceCacheLevel, preview); + if (element == null) + { + continue; + } + + contentPublishedElements[element.Key] = element; + } + + // If there are no content elements, it doesn't matter what is stored in layout + if (contentPublishedElements.Count == 0) + { + return createEmptyModel(); + } + + // Convert the settings data + var settingsPublishedElements = new Dictionary(); + var validSettingsElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey) + .Where(x => x.HasValue).Distinct().ToList(); + foreach (BlockItemData data in converted.BlockValue.SettingsData) + { + if (!validSettingsElementTypes.Contains(data.ContentTypeKey)) + { + continue; + } + + IPublishedElement? element = BlockEditorConverter.ConvertToElement(data, referenceCacheLevel, preview); + if (element is null) + { + continue; + } + + settingsPublishedElements[element.Key] = element; + } + + BlockItemActivator blockItemActivator = CreateBlockItemActivator(); + + TBlockItemModel? CreateBlockItem(TBlockLayoutItem layoutItem) + { + // Get the content reference + var contentGuidUdi = (GuidUdi?)layoutItem.ContentUdi; + if (contentGuidUdi is null || + !contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out IPublishedElement? contentData)) + { + return null; + } + + if (!blockConfigMap.TryGetValue( + contentData.ContentType.Key, + out TBlockConfiguration? blockConfig)) + { + return null; + } + + // Get the setting reference + IPublishedElement? settingsData = null; + var settingGuidUdi = (GuidUdi?)layoutItem.SettingsUdi; + if (settingGuidUdi is not null) + { + settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData); + } + + // This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again + // We also ensure that the content types match, since maybe the settings type has been changed after this has been persisted + if (settingsData is not null && (!blockConfig.SettingsElementTypeKey.HasValue || + settingsData.ContentType.Key != blockConfig.SettingsElementTypeKey)) + { + settingsData = null; + } + + // Create instance (use content/settings type from configuration) + var blockItem = blockItemActivator.CreateInstance(blockConfig.ContentElementTypeKey, blockConfig.SettingsElementTypeKey, contentGuidUdi, contentData, settingGuidUdi, settingsData); + if (blockItem == null) + { + return null; + } + + if (enrichBlockItem != null) + { + blockItem = enrichBlockItem(blockItem, layoutItem, blockConfig, CreateBlockItem); + } + + return blockItem; + } + + var blockItems = layout.Select(CreateBlockItem).WhereNotNull().ToList(); + return createModelFromItems(blockItems); + } + + // Cache constructors locally (it's tied to the current IPublishedSnapshot and IPublishedModelFactory) + protected abstract class BlockItemActivator + { + protected abstract Type GenericItemType { get; } + + private readonly BlockEditorConverter _blockConverter; + + private readonly + Dictionary<(Guid, Guid?), Func> + _constructorCache = new(); + + public BlockItemActivator(BlockEditorConverter blockConverter) + => _blockConverter = blockConverter; + + public T CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentUdi, IPublishedElement contentData, Udi? settingsUdi, IPublishedElement? settingsData) + { + if (!_constructorCache.TryGetValue( + (contentTypeKey, settingsTypeKey), + out Func? constructor)) + { + constructor = _constructorCache[(contentTypeKey, settingsTypeKey)] = + EmitConstructor(contentTypeKey, settingsTypeKey); + } + + return constructor(contentUdi, contentData, settingsUdi, settingsData); + } + + private Func EmitConstructor( + Guid contentTypeKey, Guid? settingsTypeKey) + { + Type contentType = _blockConverter.GetModelType(contentTypeKey); + Type settingsType = settingsTypeKey.HasValue + ? _blockConverter.GetModelType(settingsTypeKey.Value) + : typeof(IPublishedElement); + Type type = GenericItemType.MakeGenericType(contentType, settingsType); + + ConstructorInfo? constructor = + type.GetConstructor(new[] { typeof(Udi), contentType, typeof(Udi), settingsType }); + if (constructor == null) + { + throw new InvalidOperationException($"Could not find the required public constructor on {type}."); + } + + // We use unsafe here, because we know the constructor parameter count and types match + return ReflectionUtilities + .EmitConstructorUnsafe>( + constructor); + } + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs new file mode 100644 index 0000000000..b4ff9510f1 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs @@ -0,0 +1,38 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +internal class RichTextBlockPropertyValueCreator : BlockPropertyValueCreatorBase +{ + public RichTextBlockPropertyValueCreator(BlockEditorConverter blockEditorConverter) + : base(blockEditorConverter) + { + } + + public RichTextBlockModel CreateBlockModel(PropertyCacheLevel referenceCacheLevel, BlockValue blockValue, bool preview, RichTextConfiguration.RichTextBlockConfiguration[] blockConfigurations) + { + RichTextBlockModel CreateEmptyModel() => RichTextBlockModel.Empty; + + RichTextBlockModel CreateModel(IList items) => new RichTextBlockModel(items); + + RichTextBlockModel blockModel = CreateBlockModel(referenceCacheLevel, blockValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); + + return blockModel; + } + + protected override BlockEditorDataConverter CreateBlockEditorDataConverter() => new RichTextEditorBlockDataConverter(); + + protected override BlockItemActivator CreateBlockItemActivator() => new RichTextBlockItemActivator(BlockEditorConverter); + + private class RichTextBlockItemActivator : BlockItemActivator + { + public RichTextBlockItemActivator(BlockEditorConverter blockConverter) : base(blockConverter) + { + } + + protected override Type GenericItemType => typeof(RichTextBlockItem<,>); + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs new file mode 100644 index 0000000000..c7fa4e4592 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs @@ -0,0 +1,9 @@ +using System.Text.RegularExpressions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +internal static partial class RichTextParsingRegexes +{ + [GeneratedRegex(".[^\"]*)\"><\\/umb-rte-block(?:-inline)?>")] + public static partial Regex BlockRegex(); +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index 5af2520cfc..649fbf36df 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -3,9 +3,12 @@ using System.Globalization; using System.Text; +using System.Text.RegularExpressions; using HtmlAgilityPack; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Blocks; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Macros; @@ -15,8 +18,11 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Infrastructure.Macros; using Umbraco.Cms.Core.Models.DeliveryApi; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Infrastructure.Extensions; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; @@ -35,6 +41,11 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel private readonly HtmlUrlParser _urlParser; private readonly IApiRichTextElementParser _apiRichTextElementParser; private readonly IApiRichTextMarkupParser _apiRichTextMarkupParser; + private readonly IPartialViewBlockEngine _partialViewBlockEngine; + private readonly BlockEditorConverter _blockEditorConverter; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private readonly IApiElementBuilder _apiElementBuilder; private DeliveryApiSettings _deliveryApiSettings; [Obsolete("Please use the constructor that takes all arguments. Will be removed in V14.")] @@ -52,9 +63,34 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel { } + [Obsolete("Please use the constructor that takes all arguments. Will be removed in V15.")] public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, IOptionsMonitor deliveryApiSettingsMonitor) + : this( + umbracoContextAccessor, + macroRenderer, + linkParser, + urlParser, + imageSourceParser, + apiRichTextElementParser, + apiRichTextMarkupParser, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>(), + deliveryApiSettingsMonitor + ) + { + } + + public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, + HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, + IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, + IPartialViewBlockEngine partialViewBlockEngine, BlockEditorConverter blockEditorConverter, IJsonSerializer jsonSerializer, + IApiElementBuilder apiElementBuilder, ILogger logger, + IOptionsMonitor deliveryApiSettingsMonitor) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; @@ -63,6 +99,11 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel _imageSourceParser = imageSourceParser; _apiRichTextElementParser = apiRichTextElementParser; _apiRichTextMarkupParser = apiRichTextMarkupParser; + _partialViewBlockEngine = partialViewBlockEngine; + _blockEditorConverter = blockEditorConverter; + _jsonSerializer = jsonSerializer; + _apiElementBuilder = apiElementBuilder; + _logger = logger; _deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue; deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings); } @@ -73,6 +114,26 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel // to be cached at the published snapshot level, because we have no idea what the macros may depend on actually. PropertyCacheLevel.Snapshot; + // to counterweigh the cache level, we're going to do as much of the heavy lifting as we can while converting source to intermediate + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + { + if (RichTextPropertyEditorHelper.TryParseRichTextEditorValue(source, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue) is false) + { + return null; + } + + // the reference cache level is .Element here, as is also the case when rendering at property level. + RichTextBlockModel? richTextBlockModel = richTextEditorValue.Blocks is not null + ? ParseRichTextBlockModel(richTextEditorValue.Blocks, propertyType, PropertyCacheLevel.Element, preview) + : null; + + return new RichTextEditorIntermediateValue + { + Markup = richTextEditorValue.Markup, + RichTextBlockModel = richTextBlockModel + }; + } + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { @@ -90,18 +151,18 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { - var sourceString = inter?.ToString(); - if (sourceString.IsNullOrWhiteSpace()) + if (inter is not RichTextEditorIntermediateValue richTextEditorIntermediateValue + || richTextEditorIntermediateValue.Markup.IsNullOrWhiteSpace()) { // different return types for the JSON configuration forces us to have different return values for empty properties return _deliveryApiSettings.RichTextOutputAsJson is false - ? new RichTextModel { Markup = string.Empty } + ? RichTextModel.Empty() : null; } return _deliveryApiSettings.RichTextOutputAsJson is false - ? new RichTextModel { Markup = _apiRichTextMarkupParser.Parse(sourceString) } - : _apiRichTextElementParser.Parse(sourceString); + ? CreateRichTextModel(richTextEditorIntermediateValue) + : _apiRichTextElementParser.Parse(richTextEditorIntermediateValue.Markup, richTextEditorIntermediateValue.RichTextBlockModel); } // NOT thread-safe over a request because it modifies the @@ -135,12 +196,12 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel private string? Convert(object? source, bool preview) { - if (source == null) + if (source is not RichTextEditorIntermediateValue intermediateValue) { return null; } - var sourceString = source.ToString()!; + var sourceString = intermediateValue.Markup; // ensures string is parsed for {localLink} and URLs and media are resolved correctly sourceString = _linkParser.EnsureInternalLinks(sourceString, preview); @@ -150,6 +211,9 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel // ensure string is parsed for macros and macros are executed correctly sourceString = RenderRteMacros(sourceString, preview); + // render blocks + sourceString = RenderRichTextBlockModel(sourceString, intermediateValue.RichTextBlockModel); + // find and remove the rel attributes used in the Umbraco UI from img tags var doc = new HtmlDocument(); doc.LoadHtml(sourceString); @@ -192,4 +256,57 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel return sourceString; } + + private RichTextBlockModel? ParseRichTextBlockModel(BlockValue blocks, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, bool preview) + { + RichTextConfiguration? configuration = propertyType.DataType.ConfigurationAs(); + if (configuration?.Blocks?.Any() is not true) + { + return null; + } + + var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter); + return creator.CreateBlockModel(referenceCacheLevel, blocks, preview, configuration.Blocks); + } + + private string RenderRichTextBlockModel(string source, RichTextBlockModel? richTextBlockModel) + { + if (richTextBlockModel is null || richTextBlockModel.Any() is false) + { + return source; + } + + var blocksByUdi = richTextBlockModel.ToDictionary(block => block.ContentUdi); + + string RenderBlock(Match match) => + UdiParser.TryParse(match.Groups["udi"].Value, out Udi? udi) && blocksByUdi.TryGetValue(udi, out RichTextBlockItem? richTextBlockItem) + ? _partialViewBlockEngine.ExecuteAsync(richTextBlockItem).GetAwaiter().GetResult() + : string.Empty; + + return RichTextParsingRegexes.BlockRegex().Replace(source, RenderBlock); + } + + private RichTextModel CreateRichTextModel(RichTextEditorIntermediateValue richTextEditorIntermediateValue) + { + var markup = _apiRichTextMarkupParser.Parse(richTextEditorIntermediateValue.Markup); + + ApiBlockItem[] blocks = richTextEditorIntermediateValue.RichTextBlockModel is not null + ? richTextEditorIntermediateValue.RichTextBlockModel + .Select(item => item.CreateApiBlockItem(_apiElementBuilder)) + .ToArray() + : Array.Empty(); + + return new RichTextModel + { + Markup = markup, + Blocks = blocks + }; + } + + private class RichTextEditorIntermediateValue + { + public required string Markup { get; set; } + + public required RichTextBlockModel? RichTextBlockModel { get; set; } + } } diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs new file mode 100644 index 0000000000..a1247313d2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs @@ -0,0 +1,125 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +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.Web; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Routing +{ + internal class RedirectTracker : IRedirectTracker + { + private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly ILocalizationService _localizationService; + private readonly IRedirectUrlService _redirectUrlService; + private readonly ILogger _logger; + + public RedirectTracker( + IUmbracoContextFactory umbracoContextFactory, + IVariationContextAccessor variationContextAccessor, + ILocalizationService localizationService, + IRedirectUrlService redirectUrlService, + ILogger logger) + { + _umbracoContextFactory = umbracoContextFactory; + _variationContextAccessor = variationContextAccessor; + _localizationService = localizationService; + _redirectUrlService = redirectUrlService; + _logger = logger; + } + + /// + public void StoreOldRoute(IContent entity, Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes) + { + using UmbracoContextReference reference = _umbracoContextFactory.EnsureUmbracoContext(); + IPublishedContentCache? contentCache = reference.UmbracoContext.Content; + IPublishedContent? entityContent = contentCache?.GetById(entity.Id); + if (entityContent is null) + { + return; + } + + // Get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures) + var defaultCultures = new Lazy(() => entityContent.AncestorsOrSelf().FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty()); + + // Get all language ISO codes (in case we're dealing with invariant content with variant ancestors) + var languageIsoCodes = new Lazy(() => _localizationService.GetAllLanguages().Select(x => x.IsoCode).ToArray()); + + foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor)) + { + // If this entity defines specific cultures, use those instead of the default ones + IEnumerable cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures.Value; + + foreach (var culture in cultures) + { + try + { + var route = contentCache?.GetRouteById(publishedContent.Id, culture); + if (IsValidRoute(route)) + { + oldRoutes[(publishedContent.Id, culture)] = (publishedContent.Key, route); + } + else if (string.IsNullOrEmpty(culture)) + { + // Retry using all languages, if this is invariant but has a variant ancestor. + foreach (string languageIsoCode in languageIsoCodes.Value) + { + route = contentCache?.GetRouteById(publishedContent.Id, languageIsoCode); + if (IsValidRoute(route)) + { + oldRoutes[(publishedContent.Id, languageIsoCode)] = (publishedContent.Key, route); + } + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not register redirects because the old route couldn't be retrieved for content ID {ContentId} and culture '{Culture}'.", publishedContent.Id, culture); + } + } + } + } + + /// + public void CreateRedirects(IDictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes) + { + if (!oldRoutes.Any()) + { + return; + } + + using UmbracoContextReference reference = _umbracoContextFactory.EnsureUmbracoContext(); + IPublishedContentCache? contentCache = reference.UmbracoContext.Content; + if (contentCache == null) + { + _logger.LogWarning("Could not track redirects because there is no published content cache available on the current published snapshot."); + return; + } + + foreach (((int contentId, string culture), (Guid contentKey, string oldRoute)) in oldRoutes) + { + try + { + var newRoute = contentCache.GetRouteById(contentId, culture); + if (!IsValidRoute(newRoute) || oldRoute == newRoute) + { + continue; + } + + _redirectUrlService.Register(oldRoute, contentKey, culture); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not track redirects because the new route couldn't be retrieved for content ID {ContentId} and culture '{Culture}'.", contentId, culture); + } + } + } + + private static bool IsValidRoute([NotNullWhen(true)] string? route) => route is not null && !route.StartsWith("err/"); + } +} diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs b/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs index 0e4ad2e9c6..c2bb444e2c 100644 --- a/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs +++ b/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs @@ -1,17 +1,11 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Routing; @@ -30,44 +24,17 @@ public sealed class RedirectTrackingHandler : INotificationHandler { private const string NotificationStateKey = "Umbraco.Cms.Core.Routing.RedirectTrackingHandler"; - private readonly ILogger _logger; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IRedirectUrlService _redirectUrlService; - private readonly IVariationContextAccessor _variationContextAccessor;private readonly ILocalizationService _localizationService; + private readonly IOptionsMonitor _webRoutingSettings; + private readonly IRedirectTracker _redirectTracker; public RedirectTrackingHandler( - ILogger logger, IOptionsMonitor webRoutingSettings, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IRedirectUrlService redirectUrlService, - IVariationContextAccessor variationContextAccessor, - ILocalizationService localizationService){ - _logger = logger; + IRedirectTracker redirectTracker) + { _webRoutingSettings = webRoutingSettings; - _publishedSnapshotAccessor = publishedSnapshotAccessor; - _redirectUrlService = redirectUrlService; - _variationContextAccessor = variationContextAccessor; - _localizationService = localizationService; - } - - [Obsolete("Use ctor with all params")] - public RedirectTrackingHandler( - ILogger logger, - IOptionsMonitor webRoutingSettings, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - IRedirectUrlService redirectUrlService, - IVariationContextAccessor variationContextAccessor) - :this( - logger, - webRoutingSettings, - publishedSnapshotAccessor, - redirectUrlService, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService()) - { - - } + _redirectTracker = redirectTracker; + } public void Handle(ContentMovedNotification notification) => CreateRedirectsForOldRoutes(notification); @@ -79,150 +46,40 @@ public sealed class RedirectTrackingHandler : public void Handle(ContentPublishingNotification notification) => StoreOldRoutes(notification.PublishedEntities, notification); - private static bool IsNotRoute(string? route) => - - // null if content not found - // err/- if collision or anomaly or ... - route == null || route.StartsWith("err/"); - private void StoreOldRoutes(IEnumerable entities, IStatefulNotification notification) { - // don't let the notification handlers kick in if Redirect Tracking is turned off in the config + // Don't let the notification handlers kick in if redirect tracking is turned off in the config. if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking) { return; } - OldRoutesDictionary oldRoutes = GetOldRoutes(notification); + Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes = GetOldRoutes(notification); foreach (IContent entity in entities) { - StoreOldRoute(entity, oldRoutes); + _redirectTracker.StoreOldRoute(entity, oldRoutes); } } private void CreateRedirectsForOldRoutes(IStatefulNotification notification) { - // don't let the notification handlers kick in if Redirect Tracking is turned off in the config + // Don't let the notification handlers kick in if redirect tracking is turned off in the config. if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking) { return; } - OldRoutesDictionary oldRoutes = GetOldRoutes(notification); - CreateRedirects(oldRoutes); + Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> oldRoutes = GetOldRoutes(notification); + _redirectTracker.CreateRedirects(oldRoutes); } - private OldRoutesDictionary GetOldRoutes(IStatefulNotification notification) + private Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)> GetOldRoutes(IStatefulNotification notification) { if (notification.State.ContainsKey(NotificationStateKey) == false) { - notification.State[NotificationStateKey] = new OldRoutesDictionary(); + notification.State[NotificationStateKey] = new Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>(); } - return (OldRoutesDictionary?)notification.State[NotificationStateKey] ?? new OldRoutesDictionary(); - } - - private void StoreOldRoute(IContent entity, OldRoutesDictionary oldRoutes) - { - if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) - { - return; - } - - IPublishedContentCache? contentCache = publishedSnapshot?.Content; - IPublishedContent? entityContent = contentCache?.GetById(entity.Id); - if (entityContent is null) - { - return; - } - - // get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures) - var defaultCultures = entityContent.AncestorsOrSelf().FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys - .ToArray() - ?? Array.Empty(); - - foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor)) - { - // if this entity defines specific cultures, use those instead of the default ones - IEnumerable cultures = - publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures; - - foreach (var culture in cultures) - { - var route = contentCache?.GetRouteById(publishedContent.Id, culture); - if (!IsNotRoute(route)) - { - oldRoutes[new ContentIdAndCulture(publishedContent.Id, culture)] = new ContentKeyAndOldRoute(publishedContent.Key, route!); - } - else if (string.IsNullOrEmpty(culture)) - { - // Retry using all languages, if this is invariant but has a variant ancestor - var languages = _localizationService.GetAllLanguages(); - foreach (var language in languages) - { - route = contentCache?.GetRouteById(publishedContent.Id, language.IsoCode); - if (!IsNotRoute(route)) - { - oldRoutes[new ContentIdAndCulture(publishedContent.Id, language.IsoCode)] = - new ContentKeyAndOldRoute(publishedContent.Key, route!); - } - } - }} - } - } - - private void CreateRedirects(OldRoutesDictionary oldRoutes) - { - if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)) - { - return; - } - - IPublishedContentCache? contentCache = publishedSnapshot?.Content; - - if (contentCache == null) - { - _logger.LogWarning("Could not track redirects because there is no current published snapshot available."); - return; - } - - foreach (KeyValuePair oldRoute in oldRoutes) - { - var newRoute = contentCache.GetRouteById(oldRoute.Key.ContentId, oldRoute.Key.Culture); - if (IsNotRoute(newRoute) || oldRoute.Value.OldRoute == newRoute) - { - continue; - } - - _redirectUrlService.Register(oldRoute.Value.OldRoute, oldRoute.Value.ContentKey, oldRoute.Key.Culture); - } - } - - private class ContentIdAndCulture : Tuple - { - public ContentIdAndCulture(int contentId, string culture) - : base(contentId, culture) - { - } - - public int ContentId => Item1; - - public string Culture => Item2; - } - - private class ContentKeyAndOldRoute : Tuple - { - public ContentKeyAndOldRoute(Guid contentKey, string oldRoute) - : base(contentKey, oldRoute) - { - } - - public Guid ContentKey => Item1; - - public string OldRoute => Item2; - } - - private class OldRoutesDictionary : Dictionary - { + return (Dictionary<(int ContentId, string Culture), (Guid ContentKey, string OldRoute)>?)notification.State[NotificationStateKey]!; } } diff --git a/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs index 82f6e01475..8c94cb53c1 100644 --- a/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs @@ -10,10 +10,8 @@ public class ConfigurationEditorJsonSerializer : JsonNetSerializer, IConfigurati { public ConfigurationEditorJsonSerializer() { - JsonSerializerSettings.Converters.Add(new FuzzyBooleanConverter()); - JsonSerializerSettings.ContractResolver = new ConfigurationCustomContractResolver(); - JsonSerializerSettings.Formatting = Formatting.None; - JsonSerializerSettings.NullValueHandling = NullValueHandling.Ignore; + Settings.Converters.Add(new FuzzyBooleanConverter()); + Settings.ContractResolver = new ConfigurationCustomContractResolver(); } private class ConfigurationCustomContractResolver : DefaultContractResolver diff --git a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs index dfab4ba5e4..8057ba6168 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs @@ -7,23 +7,20 @@ namespace Umbraco.Cms.Infrastructure.Serialization; public class JsonNetSerializer : IJsonSerializer { - protected static readonly JsonSerializerSettings JsonSerializerSettings = new() + protected JsonSerializerSettings Settings { get; } = new() { Converters = new List { new StringEnumConverter() }, Formatting = Formatting.None, NullValueHandling = NullValueHandling.Ignore, }; - public string Serialize(object? input) => JsonConvert.SerializeObject(input, JsonSerializerSettings); + public string Serialize(object? input) => JsonConvert.SerializeObject(input, Settings); - public T? Deserialize(string input) => JsonConvert.DeserializeObject(input, JsonSerializerSettings); + public T? Deserialize(string input) => JsonConvert.DeserializeObject(input, Settings); public T? DeserializeSubset(string input, string key) { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } + ArgumentNullException.ThrowIfNull(key); JObject? root = Deserialize(input); JToken? jToken = root?.SelectToken(key); diff --git a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs new file mode 100644 index 0000000000..cf09c0d3a2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs @@ -0,0 +1,74 @@ +using System.Net.Http.Headers; +using System.Text; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Services.Implement; + +public class WebhookFiringService : IWebhookFiringService +{ + private readonly IJsonSerializer _jsonSerializer; + private readonly WebhookSettings _webhookSettings; + private readonly IWebhookLogService _webhookLogService; + private readonly IWebhookLogFactory _webhookLogFactory; + + public WebhookFiringService( + IJsonSerializer jsonSerializer, + IOptions webhookSettings, + IWebhookLogService webhookLogService, + IWebhookLogFactory webhookLogFactory) + { + _jsonSerializer = jsonSerializer; + _webhookLogService = webhookLogService; + _webhookLogFactory = webhookLogFactory; + _webhookSettings = webhookSettings.Value; + } + + // TODO: Add queing instead of processing directly in thread + // as this just makes save and publish longer + public async Task FireAsync(Webhook webhook, string eventName, object? payload, CancellationToken cancellationToken) + { + for (var retry = 0; retry < _webhookSettings.MaximumRetries; retry++) + { + HttpResponseMessage response = await SendRequestAsync(webhook, eventName, payload, retry, cancellationToken); + + if (response.IsSuccessStatusCode) + { + return; + } + } + } + + private async Task SendRequestAsync(Webhook webhook, string eventName, object? payload, int retryCount, CancellationToken cancellationToken) + { + using var httpClient = new HttpClient(); + + var serializedObject = _jsonSerializer.Serialize(payload); + var stringContent = new StringContent(serializedObject, Encoding.UTF8, "application/json"); + stringContent.Headers.TryAddWithoutValidation("Umb-Webhook-Event", eventName); + + foreach (KeyValuePair header in webhook.Headers) + { + stringContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + HttpResponseMessage response = await httpClient.PostAsync(webhook.Url, stringContent, cancellationToken); + + var webhookResponseModel = new WebhookResponseModel + { + HttpResponseMessage = response, + RetryCount = retryCount, + }; + + + WebhookLog log = await _webhookLogFactory.CreateAsync(eventName, webhookResponseModel, webhook, cancellationToken); + await _webhookLogService.CreateAsync(log); + + return response; + } +} + + diff --git a/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj b/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj deleted file mode 100644 index 803002fb98..0000000000 --- a/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - Umbraco CMS - Core - Contains the core assembly needed to run Umbraco CMS. - false - false - - - - - - - - - - diff --git a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj deleted file mode 100644 index 60b1b3388f..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - Umbraco CMS - Infrastructure - Contains the infrastructure assembly needed to run Umbraco CMS. - false - false - - - - - - - - - - diff --git a/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj b/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj deleted file mode 100644 index bddde68ce7..0000000000 --- a/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - Umbraco CMS - Web - Contains the web assembly needed to run Umbraco CMS. - false - false - - - - - - - diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 6dbd5f1e79..cc517ba178 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -585,6 +585,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers "mediaPickerThreeBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.UploadMedia(null!)) }, + { + "webhooksApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAll(0, 0)) + }, } }, { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 5e386afbcc..3776e4b703 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -771,6 +771,7 @@ public class ContentController : ContentControllerBase return pagedResult; } + /// /// Creates a blueprint from a content item /// @@ -1056,7 +1057,7 @@ public class ContentController : ContentControllerBase AddDomainWarnings(publishStatus.Content, successfulCultures, globalNotifications); AddPublishStatusNotifications(new[] { publishStatus }, globalNotifications, notifications, successfulCultures); } - break; + break; case ContentSaveAction.PublishWithDescendants: case ContentSaveAction.PublishWithDescendantsNew: { @@ -1073,7 +1074,7 @@ public class ContentController : ContentControllerBase AddDomainWarnings(publishStatus, successfulCultures, globalNotifications); AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures); } - break; + break; case ContentSaveAction.PublishWithDescendantsForce: case ContentSaveAction.PublishWithDescendantsForceNew: { @@ -1089,7 +1090,7 @@ public class ContentController : ContentControllerBase var publishStatus = PublishBranchInternal(contentItem, true, cultureForInvariantErrors, out wasCancelled, out var successfulCultures).ToList(); AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures); } - break; + break; default: throw new ArgumentOutOfRangeException(); } @@ -2743,7 +2744,7 @@ public class ContentController : ContentControllerBase } } } - break; + break; case PublishResultType.SuccessPublish: { // TODO: Here we should have messaging for when there are release dates specified like https://github.com/umbraco/Umbraco-CMS/pull/3507 @@ -2771,7 +2772,7 @@ public class ContentController : ContentControllerBase } } } - break; + break; case PublishResultType.FailedPublishPathNotPublished: { //TODO: This doesn't take into account variations with the successfulCultures param @@ -2780,14 +2781,14 @@ public class ContentController : ContentControllerBase _localizedTextService.Localize(null, "publish"), _localizedTextService.Localize("publish", "contentPublishedFailedByParent", new[] { names }).Trim()); } - break; + break; case PublishResultType.FailedPublishCancelledByEvent: { //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content?.Name}'")); AddCancelMessage(display, "publish", "contentPublishedFailedByEvent", new[] { names }); } - break; + break; case PublishResultType.FailedPublishAwaitingRelease: { //TODO: This doesn't take into account variations with the successfulCultures param @@ -2796,7 +2797,7 @@ public class ContentController : ContentControllerBase _localizedTextService.Localize(null, "publish"), _localizedTextService.Localize("publish", "contentPublishedFailedAwaitingRelease", new[] { names }).Trim()); } - break; + break; case PublishResultType.FailedPublishHasExpired: { //TODO: This doesn't take into account variations with the successfulCultures param @@ -2805,7 +2806,7 @@ public class ContentController : ContentControllerBase _localizedTextService.Localize(null, "publish"), _localizedTextService.Localize("publish", "contentPublishedFailedExpired", new[] { names }).Trim()); } - break; + break; case PublishResultType.FailedPublishIsTrashed: { //TODO: This doesn't take into account variations with the successfulCultures param @@ -2814,7 +2815,7 @@ public class ContentController : ContentControllerBase _localizedTextService.Localize(null, "publish"), _localizedTextService.Localize("publish", "contentPublishedFailedIsTrashed", new[] { names }).Trim()); } - break; + break; case PublishResultType.FailedPublishContentInvalid: { if (successfulCultures == null) @@ -2838,7 +2839,7 @@ public class ContentController : ContentControllerBase } } } - break; + break; case PublishResultType.FailedPublishMandatoryCultureMissing: display.AddWarningNotification( _localizedTextService.Localize(null, "publish"), diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index 0e613787fd..1f9e09940c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -78,7 +78,6 @@ public abstract class ContentControllerBase : BackOfficeNotificationsController ModelState.AddModelError("id", $"content with id: {id} was not found"); NotFoundObjectResult errorResponse = NotFound(ModelState); - return errorResponse; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index fedf8ceba4..38231740ba 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -9,8 +9,11 @@ using Examine.Search; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -19,9 +22,12 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Models.TemplateQuery; using Umbraco.Cms.Core.Persistence; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.DynamicRoot; +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.Xml; @@ -62,6 +68,7 @@ public class EntityController : UmbracoAuthorizedJsonController private static readonly string[] _postFilterSplitStrings = { "=", "==", "!=", "<>", ">", "<", ">=", "<=" }; private readonly AppCaches _appCaches; + private readonly IDynamicRootService _dynamicRootService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IContentService _contentService; private readonly IContentTypeService _contentTypeService; @@ -82,6 +89,7 @@ public class EntityController : UmbracoAuthorizedJsonController private readonly IUmbracoMapper _umbracoMapper; private readonly IUserService _userService; + [ActivatorUtilitiesConstructor] public EntityController( ITreeService treeService, UmbracoTreeSearcher treeSearcher, @@ -102,7 +110,8 @@ public class EntityController : UmbracoAuthorizedJsonController IMacroService macroService, IUserService userService, ILocalizationService localizationService, - AppCaches appCaches) + AppCaches appCaches, + IDynamicRootService dynamicRootService) { _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); _treeSearcher = treeSearcher ?? throw new ArgumentNullException(nameof(treeSearcher)); @@ -129,6 +138,54 @@ public class EntityController : UmbracoAuthorizedJsonController _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); + _dynamicRootService = dynamicRootService; + } + + [Obsolete("Use non-obsolete ctor. This will be removed in Umbraco 14.")] + public EntityController( + ITreeService treeService, + UmbracoTreeSearcher treeSearcher, + SearchableTreeCollection searchableTreeCollection, + IPublishedContentQuery publishedContentQuery, + IShortStringHelper shortStringHelper, + IEntityService entityService, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IPublishedUrlProvider publishedUrlProvider, + IContentService contentService, + IUmbracoMapper umbracoMapper, + IDataTypeService dataTypeService, + ISqlContext sqlContext, + ILocalizedTextService localizedTextService, + IFileService fileService, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMacroService macroService, + IUserService userService, + ILocalizationService localizationService, + AppCaches appCaches): this( + treeService, + treeSearcher, + searchableTreeCollection, + publishedContentQuery, + shortStringHelper, + entityService, + backofficeSecurityAccessor, + publishedUrlProvider, + contentService, + umbracoMapper, + dataTypeService, + sqlContext, + localizedTextService, + fileService, + contentTypeService, + mediaTypeService, + macroService, + userService, + localizationService, + appCaches, + StaticServiceProvider.Instance.GetRequiredService()) + { + } @@ -525,6 +582,52 @@ public class EntityController : UmbracoAuthorizedJsonController [Obsolete("This will be removed in Umbraco 13. Use GetByXPath instead")] public ActionResult? GetByQuery(string query, int nodeContextId, UmbracoEntityTypes type) => GetByXPath(query, nodeContextId, null, type); + public class DynamicRootViewModel + { + public DynamicRoot Query { get; set; } = null!; + + public int CurrentId { get; set; } + + public int ParentId { get; set; } + } + + [HttpPost] + public async Task> GetDynamicRootAsync([FromBody]DynamicRootViewModel model) + { + var currentKey = model.CurrentId == 0 ? null : _entityService.Get(model.CurrentId)?.Key; + var parentKey = model.ParentId == 0 ? null : _entityService.Get(model.ParentId)?.Key; + + if (parentKey is null) + { + throw new ArgumentException("Invalid parentId", nameof(model.ParentId)); + } + + var startNodeSelector = new DynamicRootNodeQuery() + { + Context = new DynamicRootContext() + { + CurrentKey = currentKey, + ParentKey = parentKey.Value + }, + OriginKey = model.Query.OriginKey, + OriginAlias = model.Query.OriginAlias, + QuerySteps = model.Query.QuerySteps.Select(x=>new DynamicRootQueryStep() + { + Alias = x.Alias, + AnyOfDocTypeKeys = x.AnyOfDocTypeKeys + }) + }; + var startNodes = (await _dynamicRootService.GetDynamicRootsAsync(startNodeSelector)).ToArray(); + + Guid? first = startNodes.Any() ? startNodes.First() : null; + if (first.HasValue) + { + return GetById(first.Value, UmbracoEntityTypes.Document); + } + + return Ok(); + } + /// /// Gets an entity by a xpath query /// @@ -1630,3 +1733,5 @@ public class EntityController : UmbracoAuthorizedJsonController #endregion } + + diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs new file mode 100644 index 0000000000..9be5372ce5 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Models; + +namespace Umbraco.Cms.Web.BackOffice.Controllers; + +[PluginController(Constants.Web.Mvc.BackOfficeApiArea)] +public class WebhookController : UmbracoAuthorizedJsonController +{ + private readonly IWebHookService _webHookService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly WebhookEventCollection _webhookEventCollection; + private readonly IWebhookLogService _webhookLogService; + + public WebhookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection, IWebhookLogService webhookLogService) + { + _webHookService = webHookService; + _umbracoMapper = umbracoMapper; + _webhookEventCollection = webhookEventCollection; + _webhookLogService = webhookLogService; + } + + [HttpGet] + public async Task GetAll(int skip = 0, int take = int.MaxValue) + { + PagedModel webhooks = await _webHookService.GetAllAsync(skip, take); + + List webhookViewModels = _umbracoMapper.MapEnumerable(webhooks.Items); + + return Ok(webhookViewModels); + } + + [HttpPut] + public async Task Update(WebhookViewModel webhookViewModel) + { + Webhook updateModel = _umbracoMapper.Map(webhookViewModel)!; + + await _webHookService.UpdateAsync(updateModel); + + return Ok(); + } + + [HttpPost] + public async Task Create(WebhookViewModel webhookViewModel) + { + Webhook webhook = _umbracoMapper.Map(webhookViewModel)!; + await _webHookService.CreateAsync(webhook); + + return Ok(); + } + + [HttpGet] + public async Task GetByKey(Guid key) + { + Webhook? webhook = await _webHookService.GetAsync(key); + + return webhook is null ? NotFound() : Ok(webhook); + } + + [HttpDelete] + public async Task Delete(Guid key) + { + await _webHookService.DeleteAsync(key); + + return Ok(); + } + + [HttpGet] + public IActionResult GetEvents() + { + List viewModels = _umbracoMapper.MapEnumerable(_webhookEventCollection.AsEnumerable()); + return Ok(viewModels); + } + + [HttpGet] + public async Task GetLogs(int skip = 0, int take = int.MaxValue) + { + PagedModel logs = await _webhookLogService.Get(skip, take); + List mappedLogs = _umbracoMapper.MapEnumerable(logs.Items); + return Ok(new PagedResult(logs.Total, 0, 0) + { + Items = mappedLogs, + }); + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 4021687826..2ba1025a6e 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -4,15 +4,16 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Infrastructure.Examine.DependencyInjection; -using Umbraco.Cms.Infrastructure.Templates.PartialViews; using Umbraco.Cms.Infrastructure.WebAssets; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.Install; +using Umbraco.Cms.Web.BackOffice.Mapping; using Umbraco.Cms.Web.BackOffice.Middleware; using Umbraco.Cms.Web.BackOffice.ModelsBuilder; using Umbraco.Cms.Web.BackOffice.Routing; @@ -93,6 +94,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.WithCollectionBuilder().Add(); // register back office trees // the collection builder only accepts types inheriting from TreeControllerBase diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 003b6676fe..5c9a96b71c 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -277,6 +277,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute if (!authorizationResult.Succeeded) { + actionContext.Result = new ForbidResult(); return false; } diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs new file mode 100644 index 0000000000..c797ce67ee --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs @@ -0,0 +1,58 @@ +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Web.Common.Models; + +namespace Umbraco.Cms.Web.BackOffice.Mapping; + +public class WebhookMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new Webhook(string.Empty), Map); + mapper.Define((_, _) => new WebhookViewModel(), Map); + mapper.Define((_, _) => new WebhookEventViewModel(), Map); + mapper.Define((_, _) => new WebhookLogViewModel(), Map); + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -Id -Key -UpdateDate + private void Map(WebhookViewModel source, Webhook target, MapperContext context) + { + target.ContentTypeKeys = source.ContentTypeKeys; + target.Events = source.Events; + target.Url = source.Url; + target.Enabled = source.Enabled; + target.Key = source.Key ?? Guid.NewGuid(); + target.Headers = source.Headers; + } + + // Umbraco.Code.MapAll + private void Map(Webhook source, WebhookViewModel target, MapperContext context) + { + target.ContentTypeKeys = source.ContentTypeKeys; + target.Events = source.Events; + target.Url = source.Url; + target.Enabled = source.Enabled; + target.Key = source.Key; + target.Headers = source.Headers; + } + + // Umbraco.Code.MapAll + private void Map(IWebhookEvent source, WebhookEventViewModel target, MapperContext context) => target.EventName = source.EventName; + + // Umbraco.Code.MapAll + private void Map(WebhookLog source, WebhookLogViewModel target, MapperContext context) + { + target.Date = source.Date; + target.EventName = source.EventName; + target.Key = source.Key; + target.RequestBody = source.RequestBody ?? string.Empty; + target.ResponseBody = source.ResponseBody; + target.RetryCount = source.RetryCount; + target.StatusCode = source.StatusCode; + target.Url = source.Url; + target.RequestHeaders = source.RequestHeaders; + target.ResponseHeaders = source.ResponseHeaders; + target.WebhookKey = source.WebhookKey; + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs new file mode 100644 index 0000000000..5f315f3cbb --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Trees; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Web.BackOffice.Trees; + +[Authorize(Policy = AuthorizationPolicies.TreeAccessLogs)] +[Tree(Constants.Applications.Settings, Constants.Trees.Webhooks, SortOrder = 9, TreeGroup = Constants.Trees.Groups.Settings)] +[PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] +[CoreTree] +public class WebhooksTreeController : TreeController +{ + private readonly IMenuItemCollectionFactory _menuItemCollectionFactory; + + public WebhooksTreeController( + ILocalizedTextService localizedTextService, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + IEventAggregator eventAggregator, + IMenuItemCollectionFactory menuItemCollectionFactory) + : base(localizedTextService, umbracoApiControllerTypeCollection, eventAggregator) => + _menuItemCollectionFactory = menuItemCollectionFactory; + + protected override ActionResult GetTreeNodes(string id, FormCollection queryStrings) => + //We don't have any child nodes & only use the root node to load a custom UI + new TreeNodeCollection(); + + protected override ActionResult GetMenuForNode(string id, FormCollection queryStrings) => + //We don't have any menu item options (such as create/delete/reload) & only use the root node to load a custom UI + _menuItemCollectionFactory.Create(); + + /// + /// Helper method to create a root model for a tree + /// + /// + protected override ActionResult CreateRootNode(FormCollection queryStrings) + { + ActionResult rootResult = base.CreateRootNode(queryStrings); + if (!(rootResult.Result is null)) + { + return rootResult; + } + + TreeNode? root = rootResult.Value; + + if (root is not null) + { + // This will load in a custom UI instead of the dashboard for the root node + root.RoutePath = $"{Constants.Applications.Settings}/{Constants.Trees.Webhooks}/overview"; + root.Icon = Constants.Icons.Webhooks; + root.HasChildren = false; + root.MenuUrl = null; + } + + return root; + } +} diff --git a/src/Umbraco.Web.Common/Blocks/PartialViewBlockEngine.cs b/src/Umbraco.Web.Common/Blocks/PartialViewBlockEngine.cs new file mode 100644 index 0000000000..069e29db92 --- /dev/null +++ b/src/Umbraco.Web.Common/Blocks/PartialViewBlockEngine.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Blocks; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Blocks; + +internal sealed class PartialViewBlockEngine : IPartialViewBlockEngine +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IModelMetadataProvider _modelMetadataProvider; + private readonly ITempDataDictionaryFactory _tempDataDictionaryFactory; + + public PartialViewBlockEngine( + IHttpContextAccessor httpContextAccessor, + IModelMetadataProvider modelMetadataProvider, + ITempDataDictionaryFactory tempDataDictionaryFactory) + { + _httpContextAccessor = httpContextAccessor; + _modelMetadataProvider = modelMetadataProvider; + _tempDataDictionaryFactory = tempDataDictionaryFactory; + } + + public async Task ExecuteAsync(IBlockReference blockReference) + { + HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext(); + RouteData currentRouteData = httpContext.GetRouteData(); + + // Check if there's proxied ViewData (i.e. returned from a SurfaceController) + ProxyViewDataFeature? proxyViewDataFeature = httpContext.Features.Get(); + ViewDataDictionary viewData = proxyViewDataFeature?.ViewData + ?? new ViewDataDictionary(_modelMetadataProvider, new ModelStateDictionary()); + viewData.Model = blockReference; + + ITempDataDictionary tempData = proxyViewDataFeature?.TempData + ?? _tempDataDictionaryFactory.GetTempData(httpContext); + + var actionContext = new ActionContext(httpContext, currentRouteData, new ControllerActionDescriptor()); + IRazorViewEngine razorViewEngine = httpContext.RequestServices.GetRequiredService(); + + var viewPath = $"~/Views/Partials/richtext/Components/{blockReference.Content.ContentType.Alias}.cshtml"; + ViewEngineResult viewResult = razorViewEngine.GetView(null, viewPath, false); + + if (viewResult.View is null) + { + throw new ArgumentException($"{viewPath} does not match any available view"); + } + + await using var writer = new StringWriter(); + + var viewContext = new ViewContext( + actionContext, + viewResult.View, + viewData, + tempData, + writer, + new HtmlHelperOptions()); + + await viewResult.View.RenderAsync(viewContext); + + return writer.ToString(); + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 74ce403dfa..5a1e7623d5 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -19,6 +19,7 @@ using Smidge.FileProcessors; using Smidge.InMemory; using Smidge.Nuglify; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Blocks; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; @@ -47,6 +48,7 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Web.Common; using Umbraco.Cms.Web.Common.ApplicationModels; using Umbraco.Cms.Web.Common.AspNetCore; +using Umbraco.Cms.Web.Common.Blocks; using Umbraco.Cms.Web.Common.Configuration; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.DependencyInjection; @@ -338,6 +340,7 @@ public static partial class UmbracoBuilderExtensions }); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // register the umbraco context factory builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs new file mode 100644 index 0000000000..441a367429 --- /dev/null +++ b/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Web.Common.Models; + +[DataContract] +public class WebhookEventViewModel +{ + [DataMember(Name = "eventName")] + public string EventName { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs new file mode 100644 index 0000000000..f9bf6762f8 --- /dev/null +++ b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs @@ -0,0 +1,40 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Web.Common.Models; + +[DataContract] +public class WebhookLogViewModel +{ + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "webhookKey")] + public Guid WebhookKey { get; set; } + + [DataMember(Name = "statusCode")] + public string StatusCode { get; set; } = string.Empty; + + [DataMember(Name = "date")] + public DateTime Date { get; set; } + + [DataMember(Name = "eventName")] + public string EventName { get; set; } = string.Empty; + + [DataMember(Name = "url")] + public string Url { get; set; } = string.Empty; + + [DataMember(Name = "retryCount")] + public int RetryCount { get; set; } + + [DataMember(Name = "requestHeaders")] + public string RequestHeaders { get; set; } = string.Empty; + + [DataMember(Name = "requestBody")] + public string RequestBody { get; set; } = string.Empty; + + [DataMember(Name = "responseHeaders")] + public string ResponseHeaders { get; set; } = string.Empty; + + [DataMember(Name = "responseBody")] + public string ResponseBody { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs new file mode 100644 index 0000000000..a0efff398b --- /dev/null +++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs @@ -0,0 +1,25 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Web.Common.Models; + +[DataContract] +public class WebhookViewModel +{ + [DataMember(Name = "key")] + public Guid? Key { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } = string.Empty; + + [DataMember(Name = "events")] + public string[] Events { get; set; } = Array.Empty(); + + [DataMember(Name = "contentTypeKeys")] + public Guid[] ContentTypeKeys { get; set; } = Array.Empty(); + + [DataMember(Name = "enabled")] + public bool Enabled { get; set; } + + [DataMember(Name = "headers")] + public IDictionary Headers { get; set; } = new Dictionary(); +} diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index a2eb211266..859f5958c9 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -37,7 +37,8 @@ module.exports = { umbraco: { files: "./src/less/belle.less", watch: "./src/**/*.less", out: "umbraco.min.css" }, rteContent: { files: "./src/less/rte-content.less", watch: "./src/less/**/*.less", out: "rte-content.css" }, icons: { files: "./src/less/icons.less", watch: "./src/less/**/*.less", out: "icons.css" }, - blockgridui: { files: "./src/views/propertyeditors/blockgrid/blockgridui.less", watch: "./src/views/propertyeditors/blockgrid/blockgridui.less", out: "blockgridui.css" } + blockgridui: { files: "./src/views/propertyeditors/blockgrid/blockgridui.less", watch: "./src/views/propertyeditors/blockgrid/blockgridui.less", out: "blockgridui.css" }, + blockrteui: { files: "./src/views/propertyeditors/rte/blockrteui.less", watch: "./src/views/propertyeditors/rte/blockrteui.less", out: "blockrteui.css" } }, // js files for backoffice diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index e55c2aac3e..7aa28aeabd 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -7,7 +7,9 @@ "name": "ui", "dependencies": { "@microsoft/signalr": "7.0.12", - "ace-builds": "1.30.0", + "@umbraco-ui/uui": "1.5.0", + "@umbraco-ui/uui-css": "1.5.0", + "ace-builds": "1.31.0", "angular": "1.8.3", "angular-animate": "1.8.3", "angular-aria": "1.8.3", @@ -47,7 +49,7 @@ "@babel/preset-env": "7.21.5", "autoprefixer": "10.4.16", "cssnano": "6.0.1", - "eslint": "8.51.0", + "eslint": "8.52.0", "gulp": "4.0.2", "gulp-angular-embed-templates": "2.3.0", "gulp-babel": "8.0.0", @@ -296,9 +298,9 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -1875,9 +1877,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1991,12 +1993,12 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -2018,9 +2020,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@jridgewell/gen-mapping": { @@ -2077,6 +2079,19 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", + "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" + }, + "node_modules/@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, "node_modules/@microsoft/signalr": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.12.tgz", @@ -2170,10 +2185,13 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.15.tgz", + "integrity": "sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/eslint": { "version": "8.44.5", @@ -2226,6 +2244,847 @@ "dev": true, "optional": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.5.tgz", + "integrity": "sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA==" + }, + "node_modules/@umbraco-ui/uui": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.5.0.tgz", + "integrity": "sha512-V9pAdCsiaBy+Vq23sZd9JJCk+TX6xMsclJtTUWhwCq8/YUh6KNERbdoVfMYGUZ1yyJ/g+yddQsWlYOxHNp8msw==", + "dependencies": { + "@umbraco-ui/uui-action-bar": "1.5.0", + "@umbraco-ui/uui-avatar": "1.5.0", + "@umbraco-ui/uui-avatar-group": "1.5.0", + "@umbraco-ui/uui-badge": "1.5.0", + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-boolean-input": "1.5.0", + "@umbraco-ui/uui-box": "1.5.0", + "@umbraco-ui/uui-breadcrumbs": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-button-group": "1.5.0", + "@umbraco-ui/uui-button-inline-create": "1.5.0", + "@umbraco-ui/uui-card": "1.5.0", + "@umbraco-ui/uui-card-content-node": "1.5.0", + "@umbraco-ui/uui-card-media": "1.5.0", + "@umbraco-ui/uui-card-user": "1.5.0", + "@umbraco-ui/uui-caret": "1.5.0", + "@umbraco-ui/uui-checkbox": "1.5.0", + "@umbraco-ui/uui-color-area": "1.5.0", + "@umbraco-ui/uui-color-picker": "1.5.0", + "@umbraco-ui/uui-color-slider": "1.5.0", + "@umbraco-ui/uui-color-swatch": "1.5.0", + "@umbraco-ui/uui-color-swatches": "1.5.0", + "@umbraco-ui/uui-combobox": "1.5.0", + "@umbraco-ui/uui-combobox-list": "1.5.0", + "@umbraco-ui/uui-css": "1.5.0", + "@umbraco-ui/uui-dialog": "1.5.0", + "@umbraco-ui/uui-dialog-layout": "1.5.0", + "@umbraco-ui/uui-file-dropzone": "1.5.0", + "@umbraco-ui/uui-file-preview": "1.5.0", + "@umbraco-ui/uui-form": "1.5.0", + "@umbraco-ui/uui-form-layout-item": "1.5.0", + "@umbraco-ui/uui-form-validation-message": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-icon-registry": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0", + "@umbraco-ui/uui-input": "1.5.0", + "@umbraco-ui/uui-input-file": "1.5.0", + "@umbraco-ui/uui-input-lock": "1.5.0", + "@umbraco-ui/uui-input-password": "1.5.0", + "@umbraco-ui/uui-keyboard-shortcut": "1.5.0", + "@umbraco-ui/uui-label": "1.5.0", + "@umbraco-ui/uui-loader": "1.5.0", + "@umbraco-ui/uui-loader-bar": "1.5.0", + "@umbraco-ui/uui-loader-circle": "1.5.0", + "@umbraco-ui/uui-menu-item": "1.5.0", + "@umbraco-ui/uui-modal": "1.5.0", + "@umbraco-ui/uui-pagination": "1.5.0", + "@umbraco-ui/uui-popover": "1.5.0", + "@umbraco-ui/uui-popover-container": "1.5.0", + "@umbraco-ui/uui-progress-bar": "1.5.0", + "@umbraco-ui/uui-radio": "1.5.0", + "@umbraco-ui/uui-range-slider": "1.5.0", + "@umbraco-ui/uui-ref": "1.5.0", + "@umbraco-ui/uui-ref-list": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0", + "@umbraco-ui/uui-ref-node-data-type": "1.5.0", + "@umbraco-ui/uui-ref-node-document-type": "1.5.0", + "@umbraco-ui/uui-ref-node-form": "1.5.0", + "@umbraco-ui/uui-ref-node-member": "1.5.0", + "@umbraco-ui/uui-ref-node-package": "1.5.0", + "@umbraco-ui/uui-ref-node-user": "1.5.0", + "@umbraco-ui/uui-scroll-container": "1.5.0", + "@umbraco-ui/uui-select": "1.5.0", + "@umbraco-ui/uui-slider": "1.5.0", + "@umbraco-ui/uui-symbol-expand": "1.5.0", + "@umbraco-ui/uui-symbol-file": "1.5.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.5.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.5.0", + "@umbraco-ui/uui-symbol-folder": "1.5.0", + "@umbraco-ui/uui-symbol-lock": "1.5.0", + "@umbraco-ui/uui-symbol-more": "1.5.0", + "@umbraco-ui/uui-symbol-sort": "1.5.0", + "@umbraco-ui/uui-table": "1.5.0", + "@umbraco-ui/uui-tabs": "1.5.0", + "@umbraco-ui/uui-tag": "1.5.0", + "@umbraco-ui/uui-textarea": "1.5.0", + "@umbraco-ui/uui-toast-notification": "1.5.0", + "@umbraco-ui/uui-toast-notification-container": "1.5.0", + "@umbraco-ui/uui-toast-notification-layout": "1.5.0", + "@umbraco-ui/uui-toggle": "1.5.0", + "@umbraco-ui/uui-visually-hidden": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-action-bar": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.5.0.tgz", + "integrity": "sha512-2B4ONNRTEtoKjnBo8mtvQo2Y9WW7LDSx6q85UuA+YEWfMOgZ0hr0lFepPg+qq/q90/8ZIoItoxRo16UFrPVaHQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button-group": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-avatar": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.5.0.tgz", + "integrity": "sha512-Iw4MQ2IMfJq590ydA6d2WXJ3gC7wO1vpA6tZj3T772B81LBZR31ftoMn3ho4cpavV5Nv4LvBnGhc2YajbsVn5A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-avatar-group": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.5.0.tgz", + "integrity": "sha512-hlmqOGLQIN8uJMoLgT+RPHFWIxi8Ridhp/MrKgEjuNF6sTu4bCQyN28XuC9JD+4vBcSjU4a893QGvckalQxZiA==", + "dependencies": { + "@umbraco-ui/uui-avatar": "1.5.0", + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-badge": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.5.0.tgz", + "integrity": "sha512-6azqqcqRzVHXYz/JfAody6kDZQG3hiBTiCS8EEYY9GcFNqh8BvFLX4yK9R6zz5BVrjgT3qkmPpE2iIpqV6J58A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-base": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.5.0.tgz", + "integrity": "sha512-HzKRvbf/aPA1y8l9ZLTvF5Up7W6jX8UwqVUr1B8lwckI6tgxOEFPqLya+U4papqZDh4wz/lysXSDESeVfUy8cw==", + "dependencies": { + "lit": "^2.3.1" + } + }, + "node_modules/@umbraco-ui/uui-boolean-input": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.5.0.tgz", + "integrity": "sha512-uhIPzi7n3Z4Li3n688Q8v3725apwasZvPntm7kMdtssXay6hUHOcor+hkpPavGXRVxZGg+9gIYRM6sQWp853cA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-box": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.5.0.tgz", + "integrity": "sha512-uTHBvwzS9pRu0MVfN74+bux6lK0m1AmY/7xor9ez9/uzDyIK096D9jSLTQkfDyngIhqnV6kFLbG7PqcfQURFJQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-css": "1.4.0" + } + }, + "node_modules/@umbraco-ui/uui-box/node_modules/@umbraco-ui/uui-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.4.0.tgz", + "integrity": "sha512-HBCFPuXJijeZbjnjdqmg3oqOGB3RmpQKT/s/Uy0TSJfaQGfz0e73o2eRghYHWF2rdqHw6brKFrZTZHBVvCE/xA==", + "dependencies": { + "lit": "^2.2.2" + } + }, + "node_modules/@umbraco-ui/uui-breadcrumbs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.5.0.tgz", + "integrity": "sha512-mXuzt5o4NZ1E/HVTLYq+TklX9VQSH5zce+Ef1t2EgUE3EFQH0fwcdCRBC9SpklueNj46ngGHmVhyfv8ekne1Wg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-button": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.5.0.tgz", + "integrity": "sha512-ujicvfqUAN0JtBcgj8OG1YcyDaArTBdP5LvNsyYB8s0dePgcws71XzJ1mbHbXhuA386ioNue04yGDL+gSFlJ/A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-button-group": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.5.0.tgz", + "integrity": "sha512-8yhFdfg7p1B8MM2fIxIlc0Mmhnx46scdGhqeRhvaQ2/dcdpVTI1j1hI2JyOM18TUhJeot4olLqwatlXxlFFT+A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-button-inline-create": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.5.0.tgz", + "integrity": "sha512-J60vRf7nzQyRYKj+qYhMQR6LrQH6PyTrxyqyfDOVGzcWKzsTuRahxuVOIOzrs489cznwRYwL11jtK32MlrSjGQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-card": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.5.0.tgz", + "integrity": "sha512-RgpnQca3rpjMG/3DAmmrExI7gmNNHBNYwfjRqgCd/3QkBwRrtT/+jdppVsGRxxW5xAN90sJ/eLP7i3F5EfWlSA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-card-content-node": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.5.0.tgz", + "integrity": "sha512-aYGeTsppWT0KS9orrqkl9DF2v5l3gSGhBJZqIPiHVBOzczYIcgLWJbdAkaCgpwh1Zacbv3tnB/76965fd4EwPw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-card": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-card-media": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.5.0.tgz", + "integrity": "sha512-0KktT0IExh06W7QP1FMNqU+tpUL1qDwWeeA19PbZPXwHg15hbSW15a+Hc4aiwqlHYHOPT2gxXoiVc7jqWlMcSQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-card": "1.5.0", + "@umbraco-ui/uui-symbol-file": "1.5.0", + "@umbraco-ui/uui-symbol-folder": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-card-user": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.5.0.tgz", + "integrity": "sha512-xJjfkRHkt2xim1o+IvEPQiTpIQR+Z9+69096ssuGb3EkxyyUsDmH3aZZH6/+LKdtKR+7mPZVJub9TTWB4VRnwQ==", + "dependencies": { + "@umbraco-ui/uui-avatar": "1.5.0", + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-card": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-caret": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.5.0.tgz", + "integrity": "sha512-4Apw4TMALEydo5o31gsIyICuPVyKvG/oySNup+5psU3apS0JDQ1RXCgGVDFoFxt5xzM+iJ6/J8ZOOILMVNFM6Q==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-checkbox": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.5.0.tgz", + "integrity": "sha512-Kve+XAIkSFG9kowbZI1MpDEKihpMTtD9q36pcHiVENqxL1+Tydy60yjy3tHV8o6uamJ8qjR6ZlvLttRwLId9tQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-boolean-input": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-color-area": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.5.0.tgz", + "integrity": "sha512-FF6PrUCBo2nOg5iLbD+iB8aa3Vh+skIfqjFsPD80qLE0sKQ/53juZCnCbvvp7Z0YmIqwBlWP7xGEzJBGfS6OlA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "colord": "^2.9.3" + } + }, + "node_modules/@umbraco-ui/uui-color-picker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.5.0.tgz", + "integrity": "sha512-y/IwXhtaQJWNjwnZtYTvv47+bsmUYJzFLtXqxGckcUmyJQvoZ6DDxslTSv1B9J3QTXU0zpakqpxPszlNNHUygw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "colord": "^2.9.3" + } + }, + "node_modules/@umbraco-ui/uui-color-slider": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.5.0.tgz", + "integrity": "sha512-nkUpUxfD7VlayBHirM56xKqi1h0Opg7Q2suzxEC4KLDVLO1+L0KzsDORn1tfeantSG0PahBMbuve1XOoOwCrAA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-color-swatch": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.5.0.tgz", + "integrity": "sha512-UDqlGmJIMGyn7C23q33v8dkJoISmIAL0XZNTiPkEhwGjKRlxkbexmGd4L4vFt+nhJDRrN86JoZ64BRTHVN8V7A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0", + "colord": "^2.9.3" + } + }, + "node_modules/@umbraco-ui/uui-color-swatches": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.5.0.tgz", + "integrity": "sha512-SvTKINbckKvqkkS4XnQfpELkW2x47CUa4PsnXqioXNIWP5sBJb9Kydiu0N1+lV57fAkteqNp+YY8mFxn3a6iPA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-color-swatch": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-combobox": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.5.0.tgz", + "integrity": "sha512-SoK4+yR0dJViXZinZ7iqowl6tvWPTTPSOBVE7FfOqOAgFoccOE/nQqjeNjSM0co80OKXqHUsh+kX/HwLjdyNEA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-combobox-list": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-scroll-container": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-combobox-list": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.5.0.tgz", + "integrity": "sha512-5cVlhnst3p6eEHFqn6O8LMswx3wdwpzlfAghleQJW+ZUIVo7ZPXznZz7+6yvnVWxnI7+xxFebHgC0KFxGMUVvg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-css": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.5.0.tgz", + "integrity": "sha512-jBSJg8KTWDG7DOVzz7A+UpMxMNHtddcLgt9k25vC4H+84xl+TN51RFTqF8C0JCZdWFK0eKWYlJsGqVrDfoVCcg==", + "dependencies": { + "lit": "^2.2.2" + } + }, + "node_modules/@umbraco-ui/uui-dialog": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.5.0.tgz", + "integrity": "sha512-m6J5i+eiLdNApryIY1KW/4kyunAuTpkcWBjQmxyESmlDIqRGdW0lqaahQvcZSZHto03jleUdH5wYTLNgKIb/rw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-css": "1.4.0" + } + }, + "node_modules/@umbraco-ui/uui-dialog-layout": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.5.0.tgz", + "integrity": "sha512-vfZ3FMzYccGBVvSSXvCeoHYX+VU8QppXtFR2OGDZwU0b8BOKtfKTP/2VLPEWCG4vJYKPmqZESo3N9bZXWDkWSg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-dialog/node_modules/@umbraco-ui/uui-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.4.0.tgz", + "integrity": "sha512-HBCFPuXJijeZbjnjdqmg3oqOGB3RmpQKT/s/Uy0TSJfaQGfz0e73o2eRghYHWF2rdqHw6brKFrZTZHBVvCE/xA==", + "dependencies": { + "lit": "^2.2.2" + } + }, + "node_modules/@umbraco-ui/uui-file-dropzone": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.5.0.tgz", + "integrity": "sha512-3rkTWidY4k2fyktRxfsMVTSvF+EIguv9p1Fga7v4DCNkplCp6OyJnwWby5F//+NvTHphaGchxZirOWMLgLyDog==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-file-preview": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.5.0.tgz", + "integrity": "sha512-Re+R8uZSD3t3jUgZvzG/DfQtihss7aw+rG41IAjmRO9wBZuUAsowfgCd2OJnuOYJXeaqOYYl+QQr7pmR2a/HNQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-symbol-file": "1.5.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.5.0", + "@umbraco-ui/uui-symbol-folder": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-form": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.5.0.tgz", + "integrity": "sha512-rbXFZzAg93/fzvNkxHavUr62DnSeWuVghd9CK9lhe6A9ER9cfjOcGn/INTYK3HHPBalay9IOq+WV1xxC5H6zyg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-form-layout-item": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.5.0.tgz", + "integrity": "sha512-owla3DWo1deVUEG0JzC7pE70h6Ll6lmbR+B+utbMdEgM6shEMdokpPioeCaXb8v7On9Whz+zJGAGBAYl/oyjug==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-form-validation-message": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-form-validation-message": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.5.0.tgz", + "integrity": "sha512-wuWCzttkUlEctqdJi9qzSzT8h10WvoK3+5usYB9V8NpdPYzOmbXU5RDYpoTWS0nPO56C6rlRlt3TH1khIQtPJA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-icon": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.5.0.tgz", + "integrity": "sha512-8Sz6PaYTC8KDCKj5ed+xnlnuh9/NOs0tQGPOma1bnVxGJN8LNjl+cJSLp+iU1m3Qq50H0TG+0K/dS3WUExjbZw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-icon-registry": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.5.0.tgz", + "integrity": "sha512-ei+HnaCKFjcCYjHYC0hqncY2vDfbgRkWhftOnrhqVZPJkE4omWDmVsLSGg/vm88ar1QleDmVj+CAa4J9T+uVeg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-icon-registry-essential": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.5.0.tgz", + "integrity": "sha512-nxNEQDI4SNBXnI2/Ov60vcdzKFyRCInwZDFNAKyt31F1yTNM0EM0ne5yV4AqM6YPOKVoWzqFcLz2rx64X+oLvQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon-registry": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-input": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.5.0.tgz", + "integrity": "sha512-TlbSIRh2Z7xJxW0GEPENd369W1hHgr9Y8IIRE5RDllXzZc8yho4QXPJSDFQTiHMf41LIkOTfIkrQst5047FiXg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-input-file": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.5.0.tgz", + "integrity": "sha512-8h/qGED5KE7sb/YE7dHapZxcWXGm0qCPJft8AGOu/ZK/WdOUV1WHynLjV4yGVZgY9PVZGc+GQTzvdgwxxpltQw==", + "dependencies": { + "@umbraco-ui/uui-action-bar": "1.5.0", + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-file-dropzone": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-input-lock": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.5.0.tgz", + "integrity": "sha512-KBhZLLD+5qyibbcp0AiJo7V4e/+GiKouGz/rCk6/3vxEKpe8CtWekcHhjrdlsHcOluQeBcb1Pdqng0wC9UTO5Q==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-input": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-input-password": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.5.0.tgz", + "integrity": "sha512-8wvQ/10jfufU0QWhK3gBVo5V/fzk4AuX8wPuieKZDY9Jnwkr7ugZ11DOJtaV3Az/4a0nrfF3TQ2gbBC7zHx2JA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0", + "@umbraco-ui/uui-input": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-keyboard-shortcut": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.5.0.tgz", + "integrity": "sha512-KVTMHl6X0T4cUA3bUgM06xzwCN3VD5W3tZloF0i6e3PTHhkyCE5tKD/2Hizm56OGb+ifaI/oN3L1m7vEPC8IHw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-label": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.5.0.tgz", + "integrity": "sha512-Sc6XuMEyivBEQDfMOA6JT7nW5H4/eD6dzUtUNabOwzCG5GUpvTMfRccpdjmzOvl9VCGNWtE9ikqCBZWexWA6YA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.5.0.tgz", + "integrity": "sha512-lhl1KqRbM5NTp08fvxgzOsbHFz04z8/WjaOar6lqNnL0R+CcFtVWQrv69Opht9Sj1NdHESmHEVnX0yodod2LhQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-loader-bar": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.5.0.tgz", + "integrity": "sha512-qUcVXi4i+ClozPc0Vfw7g90CLAQVj04F71xtatxDY5nhSWDEMEI6b/pXtN/B9TklkqfgE1mf/gRziFrpbVjLhA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-loader-circle": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.5.0.tgz", + "integrity": "sha512-059/DJDYbgOmr/LPXbiDaTkBcInmzUUu/YDtQt/SkZPCO33uuB7TDc+++cMgFYskdXBpqesNvVfZOUd4P6zJyA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-menu-item": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.5.0.tgz", + "integrity": "sha512-rmKuTz0Xgf0LyQRqs3tr2Z4O6oaNCd7UmI8kEbluk4yKpk5MU38BlFY9p39fpiEVUuzjcg9pBjrEyxrC/H9xjA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-loader-bar": "1.5.0", + "@umbraco-ui/uui-symbol-expand": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-modal": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.5.0.tgz", + "integrity": "sha512-q9g4rA8OYCPlOmZMES/O17NiAu18wtMxNHMuT6dADP2tuULE+TKT6A8vqC7aq8JkWOTAXRAFvTjTmcvm6L2pvg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-pagination": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.5.0.tgz", + "integrity": "sha512-I3gCWbyLRFvi5fAlezQZarvj7FuEZ7NVZbbKJxqEhbo1bwOxDMXlDNxIIrxSg3R8YAuDNP9Pbdw+rnQwupuOMQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-button-group": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-popover": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.5.0.tgz", + "integrity": "sha512-Ab8UL4UGxTUn6hYbTqPrMtyGpQr3Xw1E/PVKG3+j+UrNw1Ro5piKgh0TahwxLnrsXWOPXfy53oaXNYsMGenndA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-popover-container": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover-container/-/uui-popover-container-1.5.0.tgz", + "integrity": "sha512-issjf86TwvwLA6sJOs5pLRMFY+WBc4oeTZiJMz5mhZ5C5UoRmU65L6RP/0UnzZ4ZGY2Gpdh2YatNnZ7hVMg5ig==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-progress-bar": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.5.0.tgz", + "integrity": "sha512-B/v7VsBBwo19Y+4NBRllt7Ls+WLQfx6vY57rfO8MQG7zxGznxpTSIYvd3wxdRuDsFQeVwwoYjF1/YBJ7iWUnEQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-radio": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.5.0.tgz", + "integrity": "sha512-3e52VZHcgHB/17eLTmiZwdm7ENgfX6AF4Dw+8H2x8jdRjyvt8lbykCq+6xewAZFsLAu7vTOEKtd2RhQFI2+hwg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-range-slider": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.5.0.tgz", + "integrity": "sha512-oHmIoF+KrHDWiOKonIWq7n94C6CzStBXrleS6iwCgWY++ayaHKCPlCuQIYp3BmGjnMQn8Ou0r2x/RuBPuraLVQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.5.0.tgz", + "integrity": "sha512-wba/OP6b/mG5kp4bUgBBcBAAy3RWTbokVyjb52FR7nyqNMnIE/UBdgi0XeBx4j6lZeEbr5k5ZOGQ1knEHbPWyQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref-list": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.5.0.tgz", + "integrity": "sha512-sxs3hC97zDuFaV8mvXLAbqqtWk0kqDdHY9ORt9CxacdT36nQS58Sw60/plCryqoyp7P2cUZVtlEeff53OKOTCQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref-node": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.5.0.tgz", + "integrity": "sha512-bjmMgrIW+/4bmUXwMwFFaPrg2MeTxXssb6EpbBItJ+s0QhTEcTNyAD/DK3RlSMRE5VPO11sRwgCr06aIhklx0Q==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-ref": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref-node-data-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.5.0.tgz", + "integrity": "sha512-k14MI3cRELOmAwmtFeBzgCFw4+uin0JSqf85ZaqNkXSAmg+4I0ayUI6PGz+Jw66yGHvw3YNeUMKPmLO8l6M79A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref-node-document-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.5.0.tgz", + "integrity": "sha512-ouytDUaSls7Hsd0WaDy4wgfKMLpxlxx16WWyHlzX5lMyhkR+S3olyNZcgDRtz9xIQV+dVE3iDsUeQcNAigCdaw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref-node-form": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.5.0.tgz", + "integrity": "sha512-D86A1+ScVGTer2kci6Y9X4ZAhCnm4kxUi7bCFH7dn7oi/Fq8fhs3PBuA7mr1FrZgrPvXVdW+Qa7ldxxU58NIWA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref-node-member": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.5.0.tgz", + "integrity": "sha512-/UPmUNk6KP2unKnJKjr1qGkdPlFGTRj3K7H/mczCY7IbtzEccdEswWJCdUy/doIkAKbDdaqKe3/9HBoA3JtWPw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref-node-package": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.5.0.tgz", + "integrity": "sha512-XkET8XKb3XxmjlIDrmtwm9o0QsaG81bcpUBEBA/wUC0OcJNrjTKyv6ciAVDP7HaW6XpN8XwsRbqdcrYwM8lXDQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-ref-node-user": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.5.0.tgz", + "integrity": "sha512-9TrIr1JWw3cIkWfQrdv9iLRIqm/dd10d6uZEWaGJ/MuxyCywqMg/LSApV/NLapB4HXhIG4pGCiXvUa8OVW99ew==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-scroll-container": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.5.0.tgz", + "integrity": "sha512-Xj5jnmCEDyRENmWtuPI1QYEMzrmi/9/LaajkPEIZEYVu2owI940F0viS5X+X/FvKehSxoSt9ainCwkLphgzNiw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.5.0.tgz", + "integrity": "sha512-lcMiIM6WxF5YraIXAqSpujx3OJzq6Snfik0BUypTWbUZdKVQTgLPh3A6We9PdD6K64AX2Zk4eH8yhQ+5GNImzQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-slider": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.5.0.tgz", + "integrity": "sha512-Mp6xz7C7GbAuQ1Totd2WLzvS56ekx4l31mAvUvor0GqrUF/hHxwfrGZOAWoBqoTdKQAFKbZVSM782a+cwNv3hg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-symbol-expand": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.5.0.tgz", + "integrity": "sha512-ZCuGAJT2qFs4wQ6Z+g/qV3obv/SbriMnaIOGy6XTTAuMlh2+aNAwm33Je0wYKCTwHNUmnl427wTMEkQcMziD4g==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-symbol-file": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.5.0.tgz", + "integrity": "sha512-ClB/lT/ebyUBmPqExB2ZinMOo/bCMEgjGxjkXy2THX4lOLUqvjDNEKLq99MAREKSh/mmGq7iB3Z/hd9/EDu75Q==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-symbol-file-dropzone": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.5.0.tgz", + "integrity": "sha512-0YL88rFFI5SOzzORtm1VtMihN4if7r0CIRe5Q3Sv0WwHjrMfIM08DeONCgN2j+ZoKgnTvt9KpE1OGigshouRug==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-symbol-file-thumbnail": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.5.0.tgz", + "integrity": "sha512-/qkf6AdAIsRmUfsBdtFkFk5wPWw6JvSVHvgk/UvZulHHb2F8TamPSJfb6voh86Vq8DzVIcy3ZbqatxH7LZBY1g==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-symbol-folder": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.5.0.tgz", + "integrity": "sha512-Sxt4n5IBT+XIqu2nJxP4RnhourwC+1X5bD40YgUBmqZJ9KV//tox4zo2elU19WCeRZFkklZGfn2smLY1FD0OGg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-symbol-lock": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.5.0.tgz", + "integrity": "sha512-EH7tEPCB+PTyjWbW+bdekk4M5hcjvYYpCKTnl3Pdpzh0mrxHPt9xa8908JB0tG8n0m0EcP+L7k8pthUmkgpK7A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-symbol-more": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.5.0.tgz", + "integrity": "sha512-EuhU4kle4swMFZnsguWPz77rOtrk0IQcXuEA60fjzFGJCwsg7yyu9Ns209IEUsYh5ktstj8pXKT8+ZDila5umg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-symbol-sort": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.5.0.tgz", + "integrity": "sha512-/cifoZXuZbDmuZFPD0rr95Gpuy18DnboOYb/Ir6G3PANJ0fWOhzykHUrdx18ItLzhhwfE3dcZk4EWcGrEkfnfg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-table": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.5.0.tgz", + "integrity": "sha512-tjhpEzBYCQdgieoXcIgcOjROrScF0Ifutz/6gmpcdrXYbgZ+YkWX7dSLAeQj3fzGebaPbNYzGOmGZA9/opZ1rg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-tabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.5.0.tgz", + "integrity": "sha512-0D5NLufis9Tzc5Vr+fl8Z0wABHyz1Tep76Qnx0nXyYzAZvdNq2IxThHbGqA1cb+FjVJSKdfp6ONfiPc/SIVAzA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-popover-container": "1.5.0", + "@umbraco-ui/uui-symbol-more": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-tag": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.5.0.tgz", + "integrity": "sha512-OZGitHjdn4coj1x7F7zfeIx5M9NhGd8+CqpD915V9Qm8YlTQxFLq1M8tqjIxaYAB5EcHXuyzRpSUCrt/WUvipA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-textarea": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.5.0.tgz", + "integrity": "sha512-+zDqbYKYfaiG0IXEaQatUaWsD4umtkTtbCMnqVPMhxwneVoE9d69ejat2zLFUI/ERm3nKMyq/NRfxzXJgzlDng==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-toast-notification": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.5.0.tgz", + "integrity": "sha512-cFjz4/uZudR3yuSqK5gqzAio55ZOOxQAOc8bC5keS0HXL84JcDwrEP4/Nz7X/uUNUqauYZG/iBUirAvqfv7Osw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-css": "1.4.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-toast-notification-container": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.5.0.tgz", + "integrity": "sha512-AB4kwgocUeDwkxiCYNH0AOMEtExDS6sEq9sk2i8AGDAEjprAB3m0HM9AlrA+T0V1GtSuv+Q1DEuCyxnVbuK0WQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-toast-notification": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-toast-notification-layout": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.5.0.tgz", + "integrity": "sha512-rM7cGCdMolhsndfZT9zGAPI9P3bl1lNpjDhWI124Mgx+KS8t2Q2h9O+7FGqFnjCTJOQES1pdQ+enl2NxCuEkNg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-css": "1.4.0" + } + }, + "node_modules/@umbraco-ui/uui-toast-notification-layout/node_modules/@umbraco-ui/uui-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.4.0.tgz", + "integrity": "sha512-HBCFPuXJijeZbjnjdqmg3oqOGB3RmpQKT/s/Uy0TSJfaQGfz0e73o2eRghYHWF2rdqHw6brKFrZTZHBVvCE/xA==", + "dependencies": { + "lit": "^2.2.2" + } + }, + "node_modules/@umbraco-ui/uui-toast-notification/node_modules/@umbraco-ui/uui-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.4.0.tgz", + "integrity": "sha512-HBCFPuXJijeZbjnjdqmg3oqOGB3RmpQKT/s/Uy0TSJfaQGfz0e73o2eRghYHWF2rdqHw6brKFrZTZHBVvCE/xA==", + "dependencies": { + "lit": "^2.2.2" + } + }, + "node_modules/@umbraco-ui/uui-toggle": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.5.0.tgz", + "integrity": "sha512-vsJSpBSmlrLzspCa1dGQGYXfc6RwTGTzSlNQdnzzP7qefVRP4GlOaqYV0TJhHMcYdbai+iEkrLznzJQvM9JFLA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-boolean-input": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-visually-hidden": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-visually-hidden/-/uui-visually-hidden-1.5.0.tgz", + "integrity": "sha512-3Imqxp8+hvirakPogqzvRlU+uhshpGRdrEMU7phCS5VGzDEl8NL1BhxR31EQAw7DspwbD5non3ZwbTwLYydfCg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -2257,9 +3116,9 @@ } }, "node_modules/ace-builds": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.30.0.tgz", - "integrity": "sha512-ZC+G1ozrrVCVL/KPkeU9R7TEwYeNJUYRrjnEvNhF8r2+WR2tkcCjmduL8M6D3abIdf/16ccEXHtpoRBhAnTyCw==" + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.31.0.tgz", + "integrity": "sha512-nitIhcUYA6wyO3lo2WZBPX5fcjllW6XFt4EFyHwcN2Fp70/IZwz8tdw6a0+8udDEwDj/ebt3aWEClIyCs/6qYA==" }, "node_modules/acorn": { "version": "8.9.0", @@ -2340,7 +3199,7 @@ "node_modules/angular-chart.js": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/angular-chart.js/-/angular-chart.js-1.1.1.tgz", - "integrity": "sha1-SfDhjQgXYrbUyXkeSHr/L7sw9a4=", + "integrity": "sha512-6lqkeQvoEOMqtIzHLeOC68fdeqjdgeQ4b3bUG3Lm6X1Y6IBM0m91G6VuVA3EV0puwPuIWz4VYkzjd0DPHhIcpA==", "dependencies": { "angular": "1.x", "chart.js": "2.3.x" @@ -2349,7 +3208,7 @@ "node_modules/angular-chart.js/node_modules/chart.js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.3.0.tgz", - "integrity": "sha1-QEYOSOLEF8BfwzJc2E97AA3H19Y=", + "integrity": "sha512-LwJ6j1FNneojxFYewnz9QDQyjV++KN2s/Lgm0eipDUaKV3Fj5jOA3xtJg7AUGFcbhsYB4+Kn16c1bXwRxbOXow==", "dependencies": { "chartjs-color": "^2.0.0", "moment": "^2.10.6" @@ -3656,7 +4515,7 @@ "node_modules/cacheable-request": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", - "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", "dev": true, "optional": true, "dependencies": { @@ -4213,8 +5072,7 @@ "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "dev": true + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" }, "node_modules/colornames": { "version": "1.1.1", @@ -5528,9 +6386,9 @@ } }, "node_modules/engine.io": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", - "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.3.tgz", + "integrity": "sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -5541,26 +6399,26 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.2.3" + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-parser": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", - "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", "dev": true, "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/ws": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", - "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "dev": true, "engines": { "node": ">=10.0.0" @@ -5797,18 +6655,19 @@ } }, "node_modules/eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -6785,18 +7644,18 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.4.tgz", - "integrity": "sha512-fbfMDvgBNIdDJLdLOwacjFAPYt67tr31H9ZhWSm45CDAxvd0I6WTlSOUo7K2P/K5sA5JgMKG64PI3DMcaFdWpQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", + "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", "dev": true, "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - }, { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } ], "optional": true, @@ -7355,9 +8214,9 @@ } }, "node_modules/gifsicle": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-5.2.1.tgz", - "integrity": "sha512-9ewIQQCAnSmkU2DhuWafd1DdsgzAkKqIWnY+023xBLSiK9Az2TDUozWQW+SyRQgFMclbe6RQldUk/49TRO3Aqw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-5.3.0.tgz", + "integrity": "sha512-FJTpgdj1Ow/FITB7SVza5HlzXa+/lqEY0tHQazAJbuAdvyJtkH4wIdsR2K414oaTwRXHFLLF+tYbipj+OpYg+Q==", "dev": true, "hasInstallScript": true, "optional": true, @@ -7526,7 +8385,7 @@ "node_modules/glob-base": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "integrity": "sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==", "dev": true, "dependencies": { "glob-parent": "^2.0.0", @@ -7539,7 +8398,7 @@ "node_modules/glob-base/node_modules/glob-parent": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", "dev": true, "dependencies": { "is-glob": "^2.0.0" @@ -7569,7 +8428,7 @@ "node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "dependencies": { "is-glob": "^3.1.0", @@ -7591,7 +8450,7 @@ "node_modules/glob-stream": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", "dev": true, "dependencies": { "extend": "^3.0.0", @@ -7789,7 +8648,7 @@ "node_modules/gulp-angular-embed-templates": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/gulp-angular-embed-templates/-/gulp-angular-embed-templates-2.3.0.tgz", - "integrity": "sha1-wBDv3VlN7pRRMoNFN9eSOt6EDXk=", + "integrity": "sha512-D4lOP2G9JYbRpuZo6rsiF5f/PpzU1BgaaAxbgxNQtyNp4zME/E3c/0F73F5J/nK+ZRMwdYblgqa4vCgRS9iVwg==", "dev": true, "dependencies": { "gulp-util": "^3.0.6", @@ -8516,7 +9375,7 @@ "node_modules/gulp-util": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", + "integrity": "sha512-q5oWPc12lwSFS9h/4VIjG+1NuNDlJ48ywV2JKItY4Ycc/n1fXJeYPVQsfu5ZrhQi7FGSDBalwUCLar/GyHXKGw==", "deprecated": "gulp-util is deprecated - replace it, following the guidelines at https://medium.com/gulpjs/gulp-util-ca3b1f9f9ac5", "dev": true, "dependencies": { @@ -8586,7 +9445,7 @@ "node_modules/gulp-util/node_modules/lodash.template": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "integrity": "sha512-0B4Y53I0OgHUJkt+7RmlDFWKjVAI/YUpWNiL9GQz5ORDr4ttgfQGo+phBWKFLJbBdtOwgMuUkdOHOnPg45jKmQ==", "dev": true, "dependencies": { "lodash._basecopy": "^3.0.0", @@ -8717,7 +9576,7 @@ "node_modules/gulp-watch/node_modules/braces": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", "dev": true, "dependencies": { "expand-range": "^1.8.1", @@ -8802,7 +9661,7 @@ "node_modules/gulp-watch/node_modules/micromatch": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", "dev": true, "dependencies": { "arr-diff": "^2.0.0", @@ -8860,7 +9719,7 @@ "node_modules/gulp-wrap-js": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/gulp-wrap-js/-/gulp-wrap-js-0.4.1.tgz", - "integrity": "sha1-3uYqpISqupVHqT0f9c0MPQvtwDE=", + "integrity": "sha512-5bWQ6ZQrUDVN0w3ufWP1ZtY8qcGQABKCSb84++qGzyqw6F8kFVeTxIQtEqF4Qzi1YOpLo0NvPlNATqBqKpA6eg==", "dev": true, "dependencies": { "escodegen": "^1.6.1", @@ -10888,6 +11747,34 @@ "node": ">=10" } }, + "node_modules/lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "dependencies": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -11231,9 +12118,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -11902,9 +12789,9 @@ } }, "node_modules/node-notifier/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -12590,7 +13477,7 @@ "node_modules/parse-glob": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "integrity": "sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==", "dev": true, "dependencies": { "glob-base": "^0.3.0", @@ -14455,9 +15342,9 @@ "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" }, "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -14776,27 +15663,52 @@ "dev": true }, "node_modules/socket.io": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", - "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", "dev": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.2.0", - "socket.io-adapter": "~2.4.0", - "socket.io-parser": "~4.2.0" + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/socket.io-adapter": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", - "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==", - "dev": true + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, "node_modules/socket.io-parser": { "version": "4.2.4", @@ -15874,7 +16786,7 @@ "node_modules/trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==", "dev": true, "optional": true, "engines": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 82c0d66b70..e754922631 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -19,7 +19,9 @@ }, "dependencies": { "@microsoft/signalr": "7.0.12", - "ace-builds": "1.30.0", + "@umbraco-ui/uui": "1.5.0", + "@umbraco-ui/uui-css": "1.5.0", + "ace-builds": "1.31.0", "angular": "1.8.3", "angular-animate": "1.8.3", "angular-aria": "1.8.3", @@ -59,7 +61,7 @@ "@babel/preset-env": "7.21.5", "autoprefixer": "10.4.16", "cssnano": "6.0.1", - "eslint": "8.51.0", + "eslint": "8.52.0", "gulp": "4.0.2", "gulp-angular-embed-templates": "2.3.0", "gulp-babel": "8.0.0", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index 9090195d08..812fec6e9c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -17,7 +17,7 @@ angular.module("umbraco.directives") scope.isLoading = true; var promises = []; - + //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because // we have this mini content editor panel that can be launched with MNTP. @@ -26,8 +26,8 @@ angular.module("umbraco.directives") var editorConfig = scope.configuration ? scope.configuration : null; if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); - //for the grid by default, we don't want to include the macro toolbar - editorConfig.toolbar = _.without(editorConfig, "umbmacro"); + //for the grid by default, we don't want to include the macro or the block-picker toolbar + editorConfig.toolbar = _.without(editorConfig, "umbmacro", "umbblockpicker"); } //ensure the grid's global config is being passed up to the RTE, these 2 properties need to be in this format @@ -125,7 +125,7 @@ angular.module("umbraco.directives") } }); - + //when the element is disposed we need to unsubscribe! // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom // element might still be there even after the modal has been hidden. diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js index b07ab55436..69d1996e9a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js @@ -13,7 +13,7 @@ function valServerMatch(serverValidationManager) { return { - require: ['form', '^^umbProperty', '?^^umbVariantContent'], + require: ['form', '?^^umbProperty', '?^^umbVariantContent'], restrict: "A", scope: { valServerMatch: "=" @@ -22,17 +22,19 @@ function valServerMatch(serverValidationManager) { var formCtrl = ctrls[0]; var umbPropCtrl = ctrls[1]; - if (!umbPropCtrl) { + // You can skip the requirement of ^^umbProperty, by parsing the culture and segment as part of valServerMatch object. + if (!umbPropCtrl && scope.valServerMatch.culture === undefined) { + console.log("val server blocked.", scope.valServerMatch) //we cannot proceed, this validator will be disabled return; } - // optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages. + // optional reference to the variant-content-controller, needed to avoid validation when the field is invariant on non-default languages. var umbVariantCtrl = ctrls[2]; - var currentProperty = umbPropCtrl.property; - var currentCulture = currentProperty.culture; - var currentSegment = currentProperty.segment; + var currentProperty = umbPropCtrl ? umbPropCtrl.property : undefined; + var currentCulture = umbPropCtrl ? currentProperty.culture : scope.valServerMatch.culture; + var currentSegment = umbPropCtrl ? currentProperty.segment : scope.valServerMatch.segment; if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive @@ -84,9 +86,13 @@ function valServerMatch(serverValidationManager) { if (Utilities.isObject(scope.valServerMatch)) { var allowedKeys = ["contains", "prefix", "suffix"]; - Object.keys(scope.valServerMatch).forEach(matchType => { + const objectKeys = Object.keys(scope.valServerMatch); + if(objectKeys.find(x => allowedKeys.x)) { + throw "valServerMatch dictionary keys must be one of " + allowedKeys.join(); + } + objectKeys.forEach(matchType => { if (allowedKeys.indexOf(matchType) === -1) { - throw "valServerMatch dictionary keys must be one of " + allowedKeys.join(); + return; } var matchVal = scope.valServerMatch[matchType]; diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js index b0ea8be9a3..2d09b521e3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js +++ b/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js @@ -26,6 +26,9 @@ angular.module("umbraco.filters").filter("ncNodeName", function (editorState, en var currentNode = editorState.getCurrent(); + // Enable using keys with dashes: + input = input.split('-').join(''); + // Ensure a unique cache per editor instance var key = "ncNodeName_" + currentNode.key; if (ncNodeNameCache.id !== key) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 6f46268b5b..6e70f1dbb8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -366,6 +366,20 @@ function entityResource($q, $http, umbRequestHelper) { 'Failed to retrieve entity data for query ' + query); }, + getDynamicRoot: function (query, currentId, parentId) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "getDynamicRoot"), + { + query: JSON.parse(query), + parentId: parentId, + currentId: currentId + }), + 'Failed to retrieve entity data for query ' + query); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getAll diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js new file mode 100644 index 0000000000..3611e67de9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js @@ -0,0 +1,47 @@ +function webhooksResource($q, $http, umbRequestHelper) { + return { + getByKey(key) { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetByKey', {key})), + 'Failed to get webhooks' + ); + }, + getAll(pageNumber, pageSize) { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetAll', {pageNumber, pageSize})), + 'Failed to get webhooks' + ); + }, + create(webhook) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'Create'), webhook), + `Failed to save webhook id ${webhook.id}` + ); + }, + update(webhook) { + return umbRequestHelper.resourcePromise( + $http.put(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'Update'), webhook), + `Failed to save webhook id ${webhook.id}` + ); + }, + delete(key) { + return umbRequestHelper.resourcePromise( + $http.delete(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'Delete', {key})), + `Failed to delete webhook id ${key}` + ); + }, + getAllEvents() { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetEvents')), + 'Failed to get events' + ); + }, + getLogs(skip, take) { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl('webhooksApiBaseUrl', 'GetLogs', {skip, take})), + 'Failed to get logs' + ); + } + }; +} +angular.module('umbraco.resources').factory('webhooksResource', webhooksResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 606d16ad2d..d045340568 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -300,7 +300,7 @@ this.value.settingsData = this.value.settingsData || []; this.propertyEditorAlias = propertyEditorAlias; - this.blockConfigurations = blockConfigurations; + this.blockConfigurations = blockConfigurations ?? []; this.blockConfigurations.forEach(blockConfiguration => { if (blockConfiguration.view != null && blockConfiguration.view !== "") { @@ -396,18 +396,20 @@ // removing duplicates. scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); - tasks.push(contentResource.getScaffoldByKeys(-20, scaffoldKeys).then(scaffolds => { - Object.values(scaffolds).forEach(scaffold => { - // self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. - if (self.scaffolds) { - self.scaffolds.push(formatScaffoldData(scaffold)); - } - }); - }).catch( - () => { - // Do nothing if we get an error. - } - )); + if(scaffoldKeys.length > 0) { + tasks.push(contentResource.getScaffoldByKeys(-20, scaffoldKeys).then(scaffolds => { + Object.values(scaffolds).forEach(scaffold => { + // self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. + if (self.scaffolds) { + self.scaffolds.push(formatScaffoldData(scaffold)); + } + }); + }).catch( + () => { + // Do nothing if we get an error. + } + )); + } return $q.all(tasks); }, @@ -525,17 +527,20 @@ } var dataModel = getDataByUdi(contentUdi, this.value.contentData); - - if (dataModel === null) { - console.error("Couldn't find content data of UDI:", contentUdi, "layoutEntry:", layoutEntry) - return null; - } - - var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey); + var blockConfiguration = null; var contentScaffold = null; + if (dataModel === null) { + console.error("Couldn't find content data of UDI:", contentUdi, "layoutEntry:", layoutEntry) + //return null; + } else { + blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey); + } + if (blockConfiguration === null) { + if(dataModel) { console.warn("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor"); + } } else { contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); if (contentScaffold === null) { @@ -641,7 +646,7 @@ }; // first time instant update of label. blockObject.label = blockObject.content?.contentTypeName || ""; - blockObject.index = 0; + blockObject.index = 0; if (blockObject.config.label && blockObject.config.label !== "" && blockObject.config.unsupported !== true) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index cabb9b0139..6ce2a61197 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -847,6 +847,12 @@ When building a custom infinite editor view you can use the same components as a open(editor); } + function eventPicker(editor) { + editor.view = "views/common/infiniteeditors/eventpicker/eventpicker.html"; + if (!editor.size) editor.size = "small"; + open(editor); + } + /** * @ngdoc method * @name umbraco.services.editorService#sectionPicker @@ -1179,7 +1185,8 @@ When building a custom infinite editor view you can use the same components as a memberGroupPicker: memberGroupPicker, memberPicker: memberPicker, memberEditor: memberEditor, - mediaCropDetails + mediaCropDetails, + eventPicker : eventPicker }; return service; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 829b7d66a4..7767e3c17b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -9,11 +9,11 @@ * @doc https://www.tiny.cloud/docs/tinymce/6/ */ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, - $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService, mediaHelper, fileManager) { + $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService, mediaHelper, fileManager, $compile) { //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce - var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption"; + var extendedValidElements = "@[id|class|style],umb-rte-block[!data-content-udi],-umb-rte-block-inline[!data-content-udi],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption"; var fallbackStyles = [ { title: 'Headers', items: [ @@ -389,6 +389,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s var config = { inline: modeInline, plugins: plugins, + custom_elements: 'umb-rte-block,~umb-rte-block-inline', valid_elements: tinyMceConfig.validElements, invalid_elements: tinyMceConfig.inValidElements, extended_valid_elements: extendedValidElements, @@ -481,7 +482,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s Utilities.extend(config, tinyMceConfig.customConfig); } - if(!config.style_formats || !config.style_formats.length){ + if(!config.style_formats || !config.style_formats.length) { // if we have no style_formats at this point we'll revert to using the default ones (fallbackStyles) config.style_formats = fallbackStyles; } @@ -669,6 +670,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } }); }, + /** * @ngdoc method * @name umbraco.services.tinyMceService#insetMediaInEditor @@ -739,6 +741,91 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } }, + + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#createBlockPicker + * @methodOf umbraco.services.tinyMceService + * + * @description + * Creates the umbraco insert block tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + */ + createBlockPicker: function (editor, blockEditorApi, callback) { + + editor.on('preInit', function (args) { + editor.serializer.addRules('umb-rte-block'); + + /** This checks if the div is a block element*/ + editor.serializer.addNodeFilter('umb-rte-block', function (nodes, name) { + for (var i = 0; i < nodes.length; i++) { + + const blockEl = nodes[i]; + /* + const block = blockEditorApi.getBlockByContentUdi(blockEl.attr("data-content-udi")); + if(block) { + const displayAsBlock = block.config.displayInline !== true; + */ + + /* if the block is set to display inline, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ + if (blockEl.parent && blockEl.parent.name.toUpperCase() === "P") { + blockEl.parent.unwrap(); + } + //} + + } + }); + }); + + editor.ui.registry.addButton('umbblockpicker', { + icon: 'visualblocks', + tooltip: 'Insert Block', + stateSelector: 'umb-rte-block[data-content-udi], umb-rte-block-inline[data-content-udi]', + onAction: function () { + + var blockEl = editor.selection.getNode(); + var blockUdi; + + if (blockEl.nodeName === 'UMB-RTE-BLOCK' || blockEl.nodeName === 'UMB-RTE-BLOCK-INLINE') { + blockUdi = blockEl.getAttribute("data-content-udi") ?? undefined; + } + + if (callback) { + angularHelper.safeApply($rootScope, function () { + callback(blockUdi); + }); + } + } + }); + }, + + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#insetBlockInEditor + * @methodOf umbraco.services.tinyMceService + * + * @description + * Inserts the block element in tinymce plugin + * + * @param {Object} blockUdi UDI of Block to insert + */ + insertBlockInEditor: function (editor, blockContentUdi, displayInline) { + if (blockContentUdi) { + if(displayInline) { + editor.selection.setContent(''); + } else { + editor.selection.setContent(''); + } + + angularHelper.safeApply($rootScope, function () { + editor.dispatch("Change"); + }); + + } + }, + /** * @ngdoc method * @name umbraco.services.tinyMceService#createUmbracoMacro @@ -1270,9 +1357,15 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s if (!args.editor) { throw "args.editor is required"; } - //if (!args.model.value) { - // throw "args.model.value is required"; - //} + if (!args.scope) { + args.scope = $rootScope; + } + if (args.getValue && !args.setValue) { + throw "args.setValue is required when getValue is set"; + } + if (args.setValue && !args.getValue) { + throw "args.getValue is required when setValue is set"; + } // force TinyMCE to load plugins/themes from minified files (see http://archive.tinymce.com/wiki.php/api4:property.tinymce.suffix.static) args.editor.suffix = ".min"; @@ -1282,15 +1375,24 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s var unwatch = null; + const getPropertyValue = args.getValue ? args.getValue : function () { + return args.model.value + } + const setPropertyValue = args.setValue ? args.setValue : function (newVal) { + args.model.value = newVal; + } + //Starts a watch on the model value so that we can update TinyMCE if the model changes behind the scenes or from the server function startWatch() { - unwatch = $rootScope.$watch(() => args.model.value, function (newVal, oldVal) { + + unwatch = args.scope.$watch(() => getPropertyValue(), function (newVal, oldVal) { if (newVal !== oldVal) { //update the display val again if it has changed from the server; //uses an empty string in the editor when the value is null args.editor.setContent(newVal || "", { format: 'raw' }); + initBlocks(); - //we need to manually dispatch this event since it is only ever dispatchd based on loading from the DOM, this + // we need to manually dispatch this event since it is only ever dispatched based on loading from the DOM, this // is required for our plugins listening to this event to execute args.editor.dispatch('LoadContent', null); } @@ -1306,14 +1408,17 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s function syncContent() { - if (args.model.value === args.editor.getContent()) { + const content = args.editor.getContent() + + if (getPropertyValue() === content) { return; } //stop watching before we update the value stopWatch(); angularHelper.safeApply($rootScope, function () { - args.model.value = args.editor.getContent(); + + setPropertyValue(content); //make the form dirty manually so that the track changes works, setting our model doesn't trigger // the angular bits because tinymce replaces the textarea. @@ -1330,6 +1435,55 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s startWatch(); } + function initBlocks() { + + const blockEls = args.editor.contentDocument.querySelectorAll('umb-rte-block, umb-rte-block-inline'); + for (var blockEl of blockEls) { + if(!blockEl._isInitializedUmbBlock) { + const blockContentUdi = blockEl.getAttribute('data-content-udi'); + if(blockContentUdi && !blockEl.$block) { + const block = args.blockEditorApi.getBlockByContentUdi(blockContentUdi); + if(block) { + blockEl.removeAttribute('contenteditable'); + + if(block.config.displayInline && blockEl.nodeName.toLowerCase() === 'umb-rte-block') { + // Change element name: + const oldBlockEl = blockEl; + blockEl = document.createElement('umb-rte-block-inline'); + blockEl.appendChild(document.createComment("Umbraco-Block")); + blockEl.setAttribute('data-content-udi', blockContentUdi); + oldBlockEl.parentNode.replaceChild(blockEl, oldBlockEl); + } else if(!block.config.displayInline && blockEl.nodeName.toLowerCase() === 'umb-rte-block-inline') { + // Change element name: + const oldBlockEl = blockEl; + blockEl = document.createElement('umb-rte-block'); + blockEl.appendChild(document.createComment("Umbraco-Block")); + blockEl.setAttribute('data-content-udi', blockContentUdi); + oldBlockEl.parentNode.replaceChild(blockEl, oldBlockEl); + } + + blockEl.$index = block.index; + blockEl.$block = block; + blockEl.$api = args.blockEditorApi; + blockEl.$culture = args.culture; + blockEl.$segment = args.segment; + blockEl.$parentForm = args.parentForm; + blockEl.$valFormManager = args.valFormManager; + $compile(blockEl)(args.scope); + blockEl.setAttribute('contenteditable', 'false'); + //blockEl.setAttribute('draggable', 'true'); + + } else { + blockEl.removeAttribute('data-content-udi'); + args.editor.dom.remove(blockEl); + } + } else { + args.editor.dom.remove(blockEl); + } + } + } + } + // If we can not find the insert image/media toolbar button // Then we need to add an event listener to the editor // That will update native browser drag & drop events @@ -1413,12 +1567,15 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }); } + initBlocks(); + }); args.editor.on('init', function () { - if (args.model.value) { - args.editor.setContent(args.model.value); + const currentValue = getPropertyValue(); + if (currentValue) { + args.editor.setContent(currentValue); } //enable browser based spell checking @@ -1526,7 +1683,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //create link picker self.createLinkPicker(args.editor, function (currentTarget, anchorElement) { - entityResource.getAnchors(args.model.value).then(anchorValues => { + entityResource.getAnchors(getPropertyValue()).then(anchorValues => { const linkPicker = { currentTarget: currentTarget, @@ -1583,6 +1740,21 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s editorService.mediaPicker(mediaPicker); }); + + //Create the insert block plugin + self.createBlockPicker(args.editor, args.blockEditorApi, function (currentTarget, userData, imgDomElement) { + args.blockEditorApi.showCreateDialog(0, false, (newBlock) => { + // TODO: Handle if its an array: + if(Utilities.isArray(newBlock)) { + newBlock.forEach(block => { + self.insertBlockInEditor(args.editor, block.layout.contentUdi, block.config.displayInline); + }); + } else { + self.insertBlockInEditor(args.editor, newBlock.layout.contentUdi, newBlock.config.displayInline); + } + }); + }); + //Create the embedded plugin self.createInsertEmbeddedMedia(args.editor, function (activeElement, modify) { var embed = { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 30b456f15b..8bf8eecf91 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -280,6 +280,7 @@ // Map membership properties _.each(displayModel.membershipProperties, prop => { + if(prop.readonly === false){ switch (prop.alias) { case '_umb_login': saveModel.username = prop.value.trim(); @@ -303,6 +304,7 @@ saveModel.isTwoFactorEnabled = prop.value; break; } + } }); // saveModel.password = this.formatChangePasswordModel(propPass.value); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index 7e42d0e46e..0173592de6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -102,10 +102,15 @@ width: 100%; } +.umb-node-preview-add + .umb-node-preview-add { + margin-left: -1px; +} + .umb-node-preview-add:hover { color: @ui-action-discreet-type-hover; border-color: @ui-action-discreet-border-hover; text-decoration: none; + z-index:1; } .umb-node-preview-add:disabled { @@ -136,4 +141,4 @@ border: 1px solid @gray-9; padding: 12px 15px; border-radius: @baseBorderRadius; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/rte-content.less b/src/Umbraco.Web.UI.Client/src/less/rte-content.less index 5a52060a0f..ffd94ff6ad 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte-content.less @@ -48,3 +48,11 @@ color: @blueExtraDark; outline: 2px solid @pinkLight; } + + +.umb-rte.mce-content-body umb-rte-block[data-mce-selected], +.umb-rte.mce-content-body umb-rte-block-inline[data-mce-selected] { + cursor: auto; + --umb-rte-block--selected: 1; + outline: none; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index f53ed06513..4f84d80495 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -146,6 +146,17 @@ } } +.umb-rte-editor-con .tox.tox-tinymce { + border-radius: 6px; + border-width: 1px; + border-color: @inputBorder; +} + +.umb-rte-editor-con .tox:not(.tox-tinymce-inline) .tox-editor-header { + box-shadow: none; + border-bottom: 1px solid #d8d7d9; +} + .tox-tinymce-inline { z-index: 999; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.controller.js new file mode 100644 index 0000000000..67bea3c07b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.controller.js @@ -0,0 +1,102 @@ +(function () { + "use strict"; + + function LanguagePickerController($scope, languageResource, localizationService, webhooksResource) { + + var vm = this; + + vm.events = []; + vm.loading = false; + + vm.selectEvent = selectEvent; + vm.submit = submit; + vm.close = close; + + function onInit() { + + vm.loading = true; + + // set default title + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectLanguages").then(function (value) { + $scope.model.title = value; + }); + } + + // make sure we can push to something + if (!$scope.model.selection) { + $scope.model.selection = []; + } + + getAllEvents(); + vm.loading = false; + } + + function getAllEvents(){ + // get all events + webhooksResource.getAllEvents() + .then((data) => { + let selectedEvents = []; + data.forEach(function (event) { + let eventObject = { name: event.eventName, selected: false} + vm.events.push(eventObject); + if($scope.model.selectedEvents && $scope.model.selectedEvents.includes(eventObject.name)){ + selectedEvents.push(eventObject); + } + }); + + selectedEvents.forEach(function (event) { + selectEvent(event) + }); + }); + } + + function selectEvent(event) { + if (!event.selected) { + event.selected = true; + $scope.model.selection.push(event); + // Only filter if we have not selected an item yet. + if($scope.model.selection.length === 1){ + if(event.name.toLowerCase().includes("content")){ + vm.events = vm.events.filter(event => event.name.toLowerCase().includes("content")); + } + else if (event.name.toLowerCase().includes("media")){ + vm.events = vm.events.filter(event => event.name.toLowerCase().includes("media")); + } + } + } else { + + $scope.model.selection.forEach(function (selectedEvent, index) { + if (selectedEvent.name === event.name) { + event.selected = false; + $scope.model.selection.splice(index, 1); + } + }); + + if($scope.model.selection.length === 0){ + vm.events = []; + getAllEvents(); + } + } + } + + function submit(model) { + if ($scope.model.submit) { + $scope.model.selection = $scope.model.selection.map((item) => item.name) + $scope.model.submit(model); + } + } + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.EventPickerController", LanguagePickerController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.html new file mode 100644 index 0000000000..d4033784ed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.html @@ -0,0 +1,43 @@ +
+ + + + + + + + + + + + + +
    +
  • +
    + +
    +
  • +
+
+
+
+ + + + + + + + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.controller.js new file mode 100644 index 0000000000..1af37043f1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.controller.js @@ -0,0 +1,40 @@ +(function () { + "use strict"; + + function PickDynamicRootCustomStepController($scope, localizationService) { + + var vm = this; + + function onInit() { + if(!$scope.model.title) { + localizationService.localize("dynamicRoot_pickDynamicRootQueryStepTitle").then(function(value){ + $scope.model.title = value; + }); + } + if(!$scope.model.subtitle) { + localizationService.localize("dynamicRoot_pickDynamicRootQueryStepDesc").then(function(value){ + $scope.model.subtitle = value; + }); + } + } + + vm.submit = submit; + function submit() { + if ($scope.model.submit) { + $scope.model.submit($scope.model); + } + } + + vm.close = close; + function close() { + if($scope.model.close) { + $scope.model.close(); + } + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.PickDynamicRootCustomStep", PickDynamicRootCustomStepController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.html new file mode 100644 index 0000000000..3c1a590300 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.html @@ -0,0 +1,55 @@ +
+ + + + + + + + + + +
+ +
+ +
+
+
+
+ +
+ + + + + + + + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.controller.js new file mode 100644 index 0000000000..9851c5f710 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.controller.js @@ -0,0 +1,82 @@ +(function () { + "use strict"; + + function PickDynamicRootOriginController($scope, localizationService, editorService, udiParser) { + + var vm = this; + + function onInit() { + + if(!$scope.model.title) { + localizationService.localize("dynamicRoot_pickDynamicRootOriginTitle").then(function(value){ + $scope.model.title = value; + }); + } + if(!$scope.model.subtitle) { + localizationService.localize("dynamicRoot_pickDynamicRootOriginDesc").then(function(value){ + $scope.model.subtitle = value; + }); + } + + } + + vm.chooseRoot = function() { + $scope.model.value.originAlias = "Root"; + $scope.model.value.originKey = null; + vm.submit($scope.model); + } + vm.chooseParent = function() { + $scope.model.value.originAlias = "Parent"; + $scope.model.value.originKey = null; + vm.submit($scope.model); + } + vm.chooseCurrent = function() { + $scope.model.value.originAlias = "Current"; + $scope.model.value.originKey = null; + vm.submit($scope.model); + } + vm.chooseSite = function() { + $scope.model.value.originAlias = "Site"; + $scope.model.value.originKey = null; + vm.submit($scope.model); + } + vm.chooseByKey = function() { + var treePicker = { + idType: "udi", + section: $scope.model.contentType, + treeAlias: $scope.model.contentType, + multiPicker: false, + submit: function(model) { + var item = model.selection[0]; + $scope.model.value.originAlias = "ByKey"; + $scope.model.value.originKey = udiParser.parse(item.udi).value; + editorService.close(); + vm.submit($scope.model); + }, + close: function() { + editorService.close(); + } + }; + editorService.treePicker(treePicker); + } + + vm.submit = submit; + function submit(model) { + if ($scope.model.submit) { + $scope.model.submit(model); + } + } + + vm.close = close; + function close() { + if($scope.model.close) { + $scope.model.close(); + } + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.PickDynamicRootOrigin", PickDynamicRootOriginController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.html new file mode 100644 index 0000000000..2b5933f6cb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.html @@ -0,0 +1,58 @@ +
+ + + + + + + + + + +
+ + + + + +
+
+
+ +
+ + + + + + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.controller.js new file mode 100644 index 0000000000..00005aa019 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.controller.js @@ -0,0 +1,91 @@ +(function () { + "use strict"; + + function PickDynamicRootQueryStepController($scope, localizationService, editorService, udiParser) { + + var vm = this; + + function onInit() { + if(!$scope.model.title) { + localizationService.localize("dynamicRoot_pickDynamicRootQueryStepTitle").then(function(value){ + $scope.model.title = value; + }); + } + if(!$scope.model.subtitle) { + localizationService.localize("dynamicRoot_pickDynamicRootQueryStepDesc").then(function(value){ + $scope.model.subtitle = value; + }); + } + } + + vm.choose = function(queryStepAlias) { + var editor = { + multiPicker: true, + filterCssClass: "not-allowed not-published", + filter: function (item) { + // filter out folders (containers), element types (for content) + return item.nodeType === "container" || item.metaData.isElement; + }, + submit: function (model) { + var typeKeys = _.map(model.selection, function(selected) { return udiParser.parse(selected.udi).value; }); + $scope.model.value = { + alias: queryStepAlias, + anyOfDocTypeKeys: typeKeys + } + editorService.close(); + vm.submit($scope.model); + }, + close: function() { + editorService.close(); + } + }; + + switch ($scope.model.contentType) { + case "content": + editorService.contentTypePicker(editor); + break; + case "media": + editorService.mediaTypePicker(editor); + break; + } + } + + vm.chooseCustom = function() { + var customStepPicker = { + view: "views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.html", + size: "small", + value: "", + submit: function(model) { + $scope.model.value = { + alias: model.value + } + editorService.close(); + vm.submit($scope.model); + }, + close: function() { + editorService.close(); + } + }; + editorService.open(customStepPicker); + } + + vm.submit = submit; + function submit(model) { + if ($scope.model.submit) { + $scope.model.submit(model); + } + } + + vm.close = close; + function close() { + if($scope.model.close) { + $scope.model.close(); + } + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.PickDynamicRootQueryStep", PickDynamicRootQueryStepController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.html new file mode 100644 index 0000000000..a887cdd425 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.html @@ -0,0 +1,58 @@ +
+ + + + + + + + + + +
+ + + + + +
+
+
+ +
+ + + + + + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js index f9c8ae8b0e..7821ccb396 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js @@ -2,48 +2,49 @@ //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco') .controller("Umbraco.PrevalueEditors.TreeSourceController", - + function($scope, $timeout, entityResource, iconHelper, editorService, eventsService){ - if (!$scope.model) { - $scope.model = {}; - } - if (!$scope.model.value) { - $scope.model.value = { - type: "content" - }; - } - if (!$scope.model.config) { - $scope.model.config = { - idType: "udi" - }; - } + $scope.showXPath = false; - if($scope.model.value.id && $scope.model.value.type !== "member"){ - entityResource.getById($scope.model.value.id, entityType()).then(function(item){ - populate(item); - }); - } - else { - $timeout(function () { - treeSourceChanged(); - }, 100); - } + if (!$scope.model) { + $scope.model = {}; + } + if (!$scope.model.value) { + $scope.model.value = { + type: "content" + }; + } + if (!$scope.model.config) { + $scope.model.config = { + idType: "udi" + }; + } - function entityType() { - var ent = "Document"; - if($scope.model.value.type === "media"){ - ent = "Media"; - } - else if ($scope.model.value.type === "member") { - ent = "Member"; - } - return ent; - } + if($scope.model.value.id && $scope.model.value.type !== "member"){ + entityResource.getById($scope.model.value.id, entityType()).then(function(item){ + populate(item); + }); + } else { + $timeout(function () { + treeSourceChanged(); + }, 100); + } - $scope.openContentPicker =function(){ + function entityType() { + var ent = "Document"; + if($scope.model.value.type === "media"){ + ent = "Media"; + } + else if ($scope.model.value.type === "member") { + ent = "Member"; + } + return ent; + } + + $scope.openContentPicker = function() { var treePicker = { - idType: $scope.model.config.idType, + idType: $scope.model.config.idType, section: $scope.model.value.type, treeAlias: $scope.model.value.type, multiPicker: false, @@ -59,39 +60,152 @@ angular.module('umbraco') editorService.treePicker(treePicker); }; - $scope.clear = function() { - $scope.model.value.id = null; - $scope.node = null; - $scope.model.value.query = null; - - treeSourceChanged(); + $scope.chooseXPath = function() { + $scope.showXPath = true; + $scope.model.value.dynamicRoot = null; + }; + $scope.chooseDynamicStartNode = function() { + $scope.showXPath = false; + $scope.model.value.dynamicRoot = { + originAlias: "Parent", + querySteps: [] + }; }; - function treeSourceChanged() { - eventsService.emit("treeSourceChanged", { value: $scope.model.value.type }); - } + $scope.clearXPath = function() { + $scope.model.value.query = null; + $scope.showXPath = false; + }; + $scope.clearDynamicStartNode = function() { + $scope.model.value.dynamicRoot = null; + $scope.showDynamicStartNode = false; + }; + + $scope.clear = function() { + $scope.model.value.id = null; + $scope.node = null; + $scope.model.value.query = null; + $scope.model.value.dynamicRoot = null; + treeSourceChanged(); + }; + + function treeSourceChanged() { + eventsService.emit("treeSourceChanged", { value: $scope.model.value.type }); + } //we always need to ensure we dont submit anything broken - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - if($scope.model.value.type === "member"){ - $scope.model.value.id = null; - $scope.model.value.query = ""; - } - }); + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + if($scope.model.value.type === "member") { + $scope.model.value.id = null; + $scope.model.value.query = ""; + $scope.model.value.dynamicRoot = null; + } + }); - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); - function populate(item){ + function populate(item) { $scope.clear(); item.icon = iconHelper.convertFromLegacyIcon(item.icon); $scope.node = item; - $scope.node.path = ""; - $scope.model.value.id = $scope.model.config.idType === "udi" ? item.udi : item.id; - entityResource.getUrl(item.id, entityType()).then(function (data) { - $scope.node.path = data; - }); + $scope.node.path = ""; + $scope.model.value.id = $scope.model.config.idType === "udi" ? item.udi : item.id; + entityResource.getUrl(item.id, entityType()).then(function (data) { + $scope.node.path = data; + }); } + + + // Dynamic Root specific: + + $scope.dynamicRootOriginIcon = null; + $scope.$watch("model.value.dynamicRoot.originAlias", function (newVal, oldVal) { + $scope.dynamicRootOriginIcon = getIconForOriginAlias(newVal); + }) + function getIconForOriginAlias(originAlias) { + switch (originAlias) { + case "Root": + return "icon-home"; + case "Parent": + return "icon-page-up"; + case "Current": + return "icon-document"; + case "Site": + return "icon-home"; + case "ByKey": + return "icon-wand"; + } + } + $scope.getIconForQueryStepAlias = getIconForQueryStepAlias; + function getIconForQueryStepAlias(originAlias) { + switch (originAlias) { + case "NearestAncestorOrSelf": + return "icon-chevron-up"; + case "FurthestAncestorOrSelf": + return "icon-chevron-up"; + case "NearestDescendantOrSelf": + return "icon-chevron-down"; + case "FurthestDescendantOrSelf": + return "icon-chevron-down"; + } + return "icon-lab"; + } + + $scope.sortableOptionsForQuerySteps = { + axis: "y", + containment: "parent", + distance: 10, + opacity: 0.7, + tolerance: "pointer", + scroll: true, + zIndex: 6000 + }; + + $scope.removeQueryStep = function (queryStep) { + var index = $scope.model.value.dynamicRoot.querySteps.indexOf(queryStep); + if(index !== -1) { + $scope.model.value.dynamicRoot.querySteps.splice(index, 1); + } + } + + $scope.openDynamicRootOriginPicker = function() { + var originPicker = { + view: "views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.html", + contentType: $scope.model.value.type, + size: "small", + value: {...$scope.model.value.dynamicRoot}, + multiPicker: false, + submit: function(model) { + $scope.model.value.dynamicRoot = model.value; + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; + editorService.open(originPicker); + }; + + $scope.appendDynamicQueryStep = function() { + var queryStepPicker = { + view: "views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.html", + contentType: $scope.model.value.type, + size: "small", + multiPicker: false, + submit: function(model) { + if(!$scope.model.value.dynamicRoot.querySteps) { + $scope.model.value.dynamicRoot.querySteps = []; + } + $scope.model.value.dynamicRoot.querySteps.push(model.value); + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; + editorService.open(queryStepPicker); + }; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html index 0ab66c964e..32bccca77a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html @@ -6,9 +6,10 @@ - Root node + -
-
- -
- - -
+
+
+ + +
-
+
+ +
XPath Query
A placeholder finds the nearest published ID and runs its query from there, so for instance:

- +
$parent/newsArticle
- +

Will try to get the parent if available, but will then fall back to the nearest ancestor and query for all news article children there.

@@ -72,10 +85,80 @@
  • - +
  • +
    + +
    Dynamic Root Query
    + + +
    +
    + +
    +
    + + +
    +
    + {{ ("umb://" + (model.value.type === 'content' ? 'document' : model.value.type) + "/" + model.value.dynamicRoot.originKey | ncNodeName)}} +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    + +
    +
    + + {{queryStep.alias}} +
    +
    + + of type: + + + {{ key | umbCmsJoinArray:', '}} + +
    +
    +
    +
    + +
    +
    +
    + + + + +
      +
    • + + +
    • +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index b21eacfae0..c08b93fc72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -32,7 +32,7 @@ var unsubscribe = []; var modelObject; - + // Property actions: let copyAllBlocksAction = null; let deleteAllBlocksAction = null; @@ -113,7 +113,7 @@ vm.model.config.validationLimit.max == 1 && vm.model.config.blocks.length == 1 && vm.model.config.useSingleBlockMode; - + vm.blockEditorApi.singleBlockMode = vm.singleBlockMode; vm.validationLimit = vm.model.config.validationLimit; @@ -151,7 +151,7 @@ setDirty(); } }; - + copyAllBlocksAction = { labelKey: "clipboard_labelForCopyAllEntries", labelTokens: [vm.model.label], @@ -537,7 +537,7 @@ } vm.requestShowCreate = requestShowCreate; - + function requestShowCreate(createIndex, mouseEvent) { if (vm.blockTypePicker) { @@ -558,15 +558,15 @@ } } - + vm.requestShowClipboard = requestShowClipboard; - + function requestShowClipboard(createIndex) { showCreateDialog(createIndex, true); } vm.showCreateDialog = showCreateDialog; - + function showCreateDialog(createIndex, openClipboard) { if (vm.blockTypePicker) { @@ -618,7 +618,7 @@ } }, close: function() { - // if opned by a inline creator button(index less than length), we want to move the focus away, to hide line-creator. + // If opened by a inline creator button(index less than length), we want to move the focus away, to hide line-creator. if (createIndex < vm.layout.length) { vm.setBlockFocus(vm.layout[Math.max(createIndex-1, 0)].$block); } @@ -791,14 +791,14 @@ // make block model var blockObject = getBlockObject(layoutEntry); if (blockObject === null) { - // Initalization of the Block Object didnt go well, therefor we will fail the paste action. + // Initialization of the Block Object didn't go well, therefor we will fail the paste action. return false; } // set the BlockObject on our layout entry. layoutEntry.$block = blockObject; - // insert layout entry at the decired location in layout. + // insert layout entry at the desired location in layout. vm.layout.splice(index, 0, layoutEntry); vm.currentBlockInFocus = blockObject; @@ -808,7 +808,7 @@ function requestDeleteBlock(block) { if (vm.readonly) return; - + localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "contentTypeEditor_yesDelete"]).then(function (data) { const overlay = { title: data[0], @@ -864,7 +864,7 @@ if (copyAllBlocksAction) { copyAllBlocksAction.isDisabled = vm.layout.length === 0; } - + if (deleteAllBlocksAction) { deleteAllBlocksAction.isDisabled = vm.layout.length === 0 || vm.readonly; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js index 0dc74d7edf..ec58b5a37f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js @@ -9,7 +9,7 @@ * If a stylesheet is used then this uses a ShadowDom to make a scoped element. * This way the backoffice styling does not collide with the block style. */ - + angular .module("umbraco") .component("umbBlockListBlock", { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 4deb10d0de..7e4ccb71e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -98,6 +98,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso minNumber: 0, startNode: { query: "", + dynamicRoot: null, type: "content", id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker } @@ -125,7 +126,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso if ($scope.model.validation && $scope.model.validation.mandatory && !$scope.model.config.minNumber) { $scope.model.config.minNumber = 1; } - + if ($scope.model.config.multiPicker === true && $scope.umbProperty) { var propertyActions = [ removeAllEntriesAction @@ -165,7 +166,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso : $scope.model.config.startNode.type === "media" ? "Media" : "Document"; - + $scope.allowOpenButton = false; $scope.allowEditButton = entityType === "Document" && !$scope.readonly; $scope.allowRemove = !$scope.readonly; @@ -255,12 +256,47 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso dialogOptions.startNodeId = ($scope.model.config.idType === "udi" ? ent.udi : ent.id).toString(); }); } + else if ($scope.model.config.startNode.dynamicRoot) { + entityResource.getDynamicRoot( + JSON.stringify($scope.model.config.startNode.dynamicRoot), + editorState.current.id, + editorState.current.parentId, + "Document" + ).then(function (ent) { + if(ent) { + dialogOptions.startNodeId = ($scope.model.config.idType === "udi" ? ent.udi : ent.id).toString(); + } else { + console.error("The Dynamic Root query did not find any valid results"); + $scope.invalidStartNode = true; + } + }); + } + else { dialogOptions.startNodeId = $scope.model.config.startNode.id; } //dialog $scope.openCurrentPicker = function () { + if($scope.invalidStartNode) { + + localizationService.localizeMany(["dynamicRoot_noValidStartNodeTitle", "dynamicRoot_noValidStartNodeDesc"]).then(function (data) { + overlayService.open({ + title: data[0], + content: data[1], + hideSubmitButton: true, + close: () => { + overlayService.close(); + }, + submit: () => { + // close the confirmation + overlayService.close(); + } + }); + }); + return; + } + $scope.currentPicker = dialogOptions; $scope.currentPicker.submit = function (model) { @@ -351,7 +387,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso var node = entityType === "Member" ? model.memberNode : entityType === "Media" ? model.mediaNode : model.contentNode; - + // update the node item.name = node.name; @@ -556,7 +592,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso } function init() { - + userService.getCurrentUser().then(function (user) { switch (entityType) { case "Document": diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blockrteui.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blockrteui.less new file mode 100644 index 0000000000..428c3e980c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blockrteui.less @@ -0,0 +1,114 @@ +@import "../../../less/variables.less"; +@import "../../../less/mixins.less"; +@import "../../../less/icons.less"; +@import "../../../less/buttons.less"; +@import "../../../less/accessibility/sr-only.less"; + +@umb-block-rte__item_minimum_height: 48px; + + +.umb-block-rte__block { + position: relative; +} + +ng-form.ng-invalid .umb-block-rte__block--actions { + opacity: 1; +} + + +.umb-block-rte--view { + position: relative; +} +.umb-block-rte--view::after { + position:absolute; + content: ''; + inset: 0; + border-style: solid; + border-color: #6ab4f0; + border-width: calc(var(--umb-rte-block--selected, 0) * 2px); + border-radius:3px; + pointer-events:none; +} + +.umb-block-rte__block--actions { + + position: absolute; + top: 0px; + padding-top:10px;/** set to make sure programmatic scrolling gets the top of the block into view. */ + + right: 10px; + + /* + If child block, it can be hidden if a parents sets: --umb-block-rte--block-ui-display: none; + */ + display: flex; + opacity: 1; + z-index:3; + + font-size: 0; + background-color: rgba(255, 255, 255, .96); + border-radius: 16px; + align-items: center; + padding: 0 5px; + margin-top:10px; + + .action { + color: @ui-action-discreet-type; + font-size: 18px; + padding: 5px; + &:hover { + color: @ui-action-discreet-type-hover; + } + } + + .action { + position: relative; + display: inline-block; + + &.--error { + color: @errorBackground; + /** TODO: warning color class does not work in shadowDOM. */ + .show-validation-type-warning & { + color: @warningBackground; + } + } + + > .__error-badge { + position: absolute; + top: -2px; + right: -2px; + min-width: 8px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 8px; + font-weight: bold; + padding: 2px; + line-height: 8px; + background-color: @errorBackground; + .show-validation-type-warning & { + background-color: @warningBackground; + } + display: none; + font-weight: 900; + } + &.--error > .__error-badge { + display: block; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-block-rte__action--badge-bounce; + animation-timing-function: ease; + @keyframes umb-block-rte__action--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-4px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-2px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/blockrteentryeditors/labelblock/rtelabelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/blockrteentryeditors/labelblock/rtelabelblock.editor.html new file mode 100644 index 0000000000..ea8d3bffd9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/blockrteentryeditors/labelblock/rtelabelblock.editor.html @@ -0,0 +1,58 @@ + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/blockrteentryeditors/unsupportedblock/unsupportedblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/blockrteentryeditors/unsupportedblock/unsupportedblock.editor.html new file mode 100644 index 0000000000..259f3d72e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/blockrteentryeditors/unsupportedblock/unsupportedblock.editor.html @@ -0,0 +1,59 @@ + + +
    +
    + + {{block.config.label}} +
    +
    + This content is no longer supported in this context.
    + You might want to remove this block, or contact your developer to take actions for making this block available again.

    + +
    Block data:
    +
    {{block.data | json : 4 }}
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.controller.js new file mode 100644 index 0000000000..8e7892d5a0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.controller.js @@ -0,0 +1,246 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.BlockRTE.BlockConfigurationController + * @function + * + * @description + * The controller for the content type editor property settings dialog + */ + +(function () { + "use strict"; + + function TransferProperties(fromObject, toObject) { + for (var p in fromObject) { + toObject[p] = fromObject[p]; + } + } + + function BlockConfigurationController($scope, elementTypeResource, overlayService, localizationService, editorService, eventsService, udiService) { + + var unsubscribe = []; + + const vm = this; + vm.openBlock = null; + + function onInit() { + + if (!$scope.model.value) { + $scope.model.value = []; + } + + loadElementTypes(); + } + + function loadElementTypes() { + return elementTypeResource.getAll().then(elementTypes => { + vm.elementTypes = elementTypes; + }); + } + + function updateUsedElementTypes(event, args) { + var key = args.documentType.key; + for (var i = 0; i { + var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey); + overlayService.confirmDelete({ + title: data[0], + content: localizationService.tokenReplace(data[1], [contentElementType ? contentElementType.name : "(Unavailable ElementType)"]), + confirmMessage: data[2], + submit: () => { + vm.removeBlockByIndex(index); + overlayService.close(); + }, + close: overlayService.close() + }); + }); + + event.stopPropagation(); + }; + + vm.removeBlockByIndex = function (index) { + $scope.model.value.splice(index, 1); + }; + + vm.sortableOptions = { + "ui-floating": true, + items: "umb-block-card", + cursor: "grabbing", + placeholder: 'umb-block-card --sortable-placeholder' + }; + + vm.getAvailableElementTypes = function () { + return vm.elementTypes.filter(function (type) { + return !$scope.model.value.find(function (entry) { + return type.key === entry.contentElementTypeKey; + }); + }); + }; + + vm.getElementTypeByKey = function(key) { + if (vm.elementTypes) { + return vm.elementTypes.find(type => type.key === key) || null; + } + }; + + vm.openAddDialog = function () { + + localizationService.localize("blockEditor_headlineCreateBlock").then(localizedTitle => { + + const contentTypePicker = { + title: localizedTitle, + section: "settings", + treeAlias: "documentTypes", + entityType: "documentType", + isDialog: true, + filter: function (node) { + if (node.metaData.isElement === true) { + var key = udiService.getKey(node.udi); + + // If a Block with this ElementType as content already exists, we will emit it as a posible option. + return $scope.model.value.find(entry => entry.contentElementTypeKey === key); + } + return true; + }, + filterCssClass: "not-allowed", + select: function (node) { + vm.addBlockFromElementTypeKey(udiService.getKey(node.udi)); + editorService.close(); + }, + close: function () { + editorService.close(); + }, + extraActions: [ + { + style: "primary", + labelKey: "blockEditor_labelcreateNewElementType", + action: function () { + vm.createElementTypeAndCallback((documentTypeKey) => { + vm.addBlockFromElementTypeKey(documentTypeKey); + + // At this point we will close the contentTypePicker. + editorService.close(); + }); + } + } + ] + }; + + editorService.treePicker(contentTypePicker); + }); + }; + + vm.createElementTypeAndCallback = function(callback) { + const editor = { + create: true, + infiniteMode: true, + noTemplate: true, + isElement: true, + submit: function (model) { + loadElementTypes().then(() => { + callback(model.documentTypeKey); + }); + + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + } + + vm.addBlockFromElementTypeKey = function(key) { + + const blockType = { + contentElementTypeKey: key, + settingsElementTypeKey: null, + labelTemplate: "", + displayInline: false, + view: null, + stylesheet: null, + editorSize: "medium", + iconColor: null, + backgroundColor: null, + thumbnail: null + }; + + $scope.model.value.push(blockType); + + vm.openBlockOverlay(blockType); + }; + + vm.openBlockOverlay = function (block) { + + var elementType = vm.getElementTypeByKey(block.contentElementTypeKey); + + if (elementType) { + + let clonedBlockData = Utilities.copy(block); + vm.openBlock = block; + + const overlayModel = { + block: clonedBlockData, + + view: "views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.html", + size: "small", + submit: function(overlayModel) { + loadElementTypes()// lets load elementType again, to ensure we are up to date. + TransferProperties(overlayModel.block, block);// transfer properties back to block object. (Doing this cause we dont know if block object is added to model jet, therefor we cant use index or replace the object.) + overlayModel.close(); + }, + close: function() { + editorService.close(); + vm.openBlock = null; + } + }; + + localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [elementType.name]).then(data => { + overlayModel.title = data, + + // open property settings editor + editorService.open(overlayModel); + }); + } else { + + const overlay = { + close: () => { + overlayService.close() + } + }; + + localizationService.localize("blockEditor_elementTypeDoesNotExist").then(data => { + overlay.content = data; + overlayService.open(overlay); + }); + } + + }; + + $scope.$on('$destroy', function () { + unsubscribe.forEach(u => { u(); }); + }); + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockRTE.BlockConfigurationController", BlockConfigurationController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.html new file mode 100644 index 0000000000..c7aed33262 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.html @@ -0,0 +1,25 @@ +
    + +
    + + +
    + +
    +
    + + +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.controller.js new file mode 100644 index 0000000000..fc8e0ae547 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.controller.js @@ -0,0 +1,314 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.BlockRTE.BlockConfigurationOverlayController + * @function + * + * @description + * The controller for the content type editor property settings dialog + */ + +(function () { + "use strict"; + + function BlockConfigurationOverlayController($scope, overlayService, localizationService, editorService, elementTypeResource, eventsService, udiService, angularHelper) { + + var unsubscribe = []; + + var vm = this; + vm.block = $scope.model.block; + + vm.colorPickerOptions = { + type: "color", + allowEmpty: true, + showAlpha: true + }; + + loadElementTypes(); + + function loadElementTypes() { + return elementTypeResource.getAll().then(function(elementTypes) { + vm.elementTypes = elementTypes; + + vm.contentPreview = vm.getElementTypeByKey(vm.block.contentElementTypeKey); + vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); + }); + } + + vm.getElementTypeByKey = function(key) { + return vm.elementTypes.find(function (type) { + return type.key === key; + }); + }; + + vm.openElementType = function(elementTypeKey) { + var elementType = vm.getElementTypeByKey(elementTypeKey); + if (elementType) { + var elementTypeId = elementType.id; + const editor = { + id: elementTypeId, + submit: function (model) { + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + } + }; + + vm.createElementTypeAndCallback = function(callback) { + const editor = { + create: true, + infiniteMode: true, + isElement: true, + noTemplate: true, + submit: function (model) { + callback(model.documentTypeKey); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + }; + + vm.addSettingsForBlock = function($event, block) { + + localizationService.localize("blockEditor_headlineAddSettingsElementType").then(localizedTitle => { + + const settingsTypePicker = { + title: localizedTitle, + entityType: "documentType", + isDialog: true, + filter: node => { + if (node.metaData.isElement === true) { + return false; + } + return true; + }, + filterCssClass: "not-allowed", + select: node => { + vm.applySettingsToBlock(block, udiService.getKey(node.udi)); + editorService.close(); + }, + close: () => editorService.close(), + extraActions: [ + { + style: "primary", + labelKey: "blockEditor_labelcreateNewElementType", + action: () => { + vm.createElementTypeAndCallback((key) => { + vm.applySettingsToBlock(block, key); + + // At this point we will close the contentTypePicker. + editorService.close(); + }); + } + } + ] + }; + + editorService.contentTypePicker(settingsTypePicker); + + }); + }; + + vm.applySettingsToBlock = function(block, key) { + block.settingsElementTypeKey = key; + vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); + }; + + vm.requestRemoveSettingsForBlock = function(block) { + localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { + + var settingsElementType = vm.getElementTypeByKey(block.settingsElementTypeKey); + + overlayService.confirmRemove({ + title: data[0], + content: localizationService.tokenReplace(data[1], [(settingsElementType ? settingsElementType.name : "(Unavailable ElementType)")]), + close: function () { + overlayService.close(); + }, + submit: function () { + vm.removeSettingsForBlock(block); + overlayService.close(); + } + }); + }); + }; + + vm.removeSettingsForBlock = function(block) { + block.settingsElementTypeKey = null; + }; + + function updateUsedElementTypes(event, args) { + var key = args.documentType.key; + for (var i = 0; i { + + const filePicker = { + title: localizedTitle, + isDialog: true, + filter: i => { + return !(i.name.indexOf(".html") !== -1); + }, + filterCssClass: "not-allowed", + select: node => { + const filepath = decodeURIComponent(node.id.replace(/\+/g, " ")); + block.view = "~/" + filepath.replace("wwwroot/", ""); + editorService.close(); + }, + close: () => editorService.close() + }; + + editorService.staticFilePicker(filePicker); + + }); + }; + + vm.requestRemoveViewForBlock = function(block) { + localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { + overlayService.confirmRemove({ + title: data[0], + content: localizationService.tokenReplace(data[1], [block.view]), + close: function () { + overlayService.close(); + }, + submit: function () { + vm.removeViewForBlock(block); + overlayService.close(); + } + }); + }); + }; + + vm.removeViewForBlock = function(block) { + block.view = null; + }; + + vm.addStylesheetForBlock = function(block) { + localizationService.localize("blockEditor_headlineAddCustomStylesheet").then(localizedTitle => { + + const filePicker = { + title: localizedTitle, + isDialog: true, + filter: i => { + return !(i.name.indexOf(".css") !== -1); + }, + filterCssClass: "not-allowed", + select: node => { + const filepath = decodeURIComponent(node.id.replace(/\+/g, " ")); + block.stylesheet = "~/" + filepath.replace("wwwroot/", ""); + editorService.close(); + }, + close: () => editorService.close() + }; + + editorService.staticFilePicker(filePicker); + + }); + }; + + vm.requestRemoveStylesheetForBlock = function(block) { + localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { + overlayService.confirmRemove({ + title: data[0], + content: localizationService.tokenReplace(data[1], [block.stylesheet]), + close: function () { + overlayService.close(); + }, + submit: function () { + vm.removeStylesheetForBlock(block); + overlayService.close(); + } + }); + }); + }; + + vm.removeStylesheetForBlock = function(block) { + block.stylesheet = null; + }; + + vm.addThumbnailForBlock = function(block) { + + localizationService.localize("blockEditor_headlineAddThumbnail").then(localizedTitle => { + + let allowedFileExtensions = ['jpg', 'jpeg', 'png', 'svg', 'webp', 'gif']; + + const thumbnailPicker = { + title: localizedTitle, + isDialog: true, + filter: i => { + let ext = i.name.substr((i.name.lastIndexOf('.') + 1)); + return allowedFileExtensions.includes(ext) === false; + }, + filterCssClass: "not-allowed", + select: file => { + const id = decodeURIComponent(file.id.replace(/\+/g, " ")); + block.thumbnail = "~/" + id.replace("wwwroot/", ""); + editorService.close(); + }, + close: () => editorService.close() + }; + + editorService.staticFilePicker(thumbnailPicker); + + }); + }; + + vm.removeThumbnailForBlock = function(entry) { + entry.thumbnail = null; + }; + + vm.changeIconColor = function (color) { + angularHelper.safeApply($scope, function () { + vm.block.iconColor = color ? color.toString() : null; + }); + }; + + vm.changeBackgroundColor = function (color) { + angularHelper.safeApply($scope, function () { + vm.block.backgroundColor = color ? color.toString() : null; + }); + }; + + vm.submit = function() { + if ($scope.model && $scope.model.submit) { + $scope.model.submit($scope.model); + } + }; + + vm.close = function() { + if ($scope.model && $scope.model.close) { + $scope.model.close($scope.model); + } + }; + + $scope.$on('$destroy', function() { + unsubscribe.forEach(u => { u(); }); + }); + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockRTE.BlockConfigurationOverlayController", BlockConfigurationOverlayController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.html new file mode 100644 index 0000000000..ddb1ed727e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.html @@ -0,0 +1,281 @@ +
    + +
    + + + + + + + +
    + +
    + +
    + Editor appearance +
    + +
    + + +
    +
    + +
    + +
    +
    +
    + + + +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + + + Overwrite how this block appears in the BackOffice UI. Pick a .html file containing your presentation. + +
    +
    + + +
    + +
    +
    + +
    +
    +
    + + +
    +
    + +
    +
    + + +
    + +
    +
    + +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + Data Models +
    + +
    + + +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    + +
    + + +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    + Catalogue appearance +
    + +
    + + +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + +
    +
    + + +
    + +
    +
    + +
    +
    +
    + +
    + +
    + +
    + +
    + Advanced +
    + +
    + + +
    +
    + +
    + + +
    +
    +
    + +
    + +
    + +
    +
    + + + + + + + + + + + + + + +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.less new file mode 100644 index 0000000000..d2d875aa94 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.less @@ -0,0 +1,103 @@ +.umb-block-list-block-configuration-overlay { + + + .umb-node-preview { + flex-grow: 1; + } + + .__control-actions { + position: absolute; + display: flex; + align-items: center; + top:0; + bottom: 0; + right: 0; + background-color: rgba(255, 255, 255, 0.8); + opacity: 0; + transition: opacity 120ms; + } + .controls:hover &, + .controls:focus &, + .controls:focus-within &, + .control-group:hover, + .control-group:focus, + .control-group:focus-within { + .__control-actions { + opacity: 1; + } + } + .__control-actions-btn { + position: relative; + color: @ui-action-discreet-type; + height: 32px; + width: 26px; + &:hover { + color: @ui-action-discreet-type-hover; + } + &:last-of-type { + margin-right: 7px; + } + } + + .umb-node-preview { + border-bottom: none; + } + + .__settings-input { + position: relative; + padding: 5px 8px; + margin-bottom: 10px; + color: @ui-action-discreet-type; + border: 1px dashed @ui-action-discreet-border; + width: 100%; + font-weight: bold; + display: inline-flex; + flex-flow: row nowrap; + + localize { + width: 100%; + } + + .umb-node-preview { + padding: 3px 0; + margin-left: 5px; + overflow: hidden; + } + + &.--noValue { + text-align: center; + border-radius: @baseBorderRadius; + color: white; + transition: color 120ms; + &:hover, &:focus { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + } + } + + &.--hasValue { + border: 1px solid @inputBorder; + padding: 0; + } + } + + .__add-button { + width:100%; + color: @ui-action-discreet-type; + border: 1px dashed @ui-action-discreet-border; + border-radius: @baseBorderRadius; + display: flex; + align-items: center; + justify-content: center; + padding: 5px 15px; + box-sizing: border-box; + margin: 20px 0; + font-weight: bold; + } + + .__add-button:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block.component.js new file mode 100644 index 0000000000..0c4c8e2e50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block.component.js @@ -0,0 +1,127 @@ +(function () { + 'use strict'; + + /** + * A component to render the property action toggle + */ + + function umbRteBlockController($scope, $compile, $element) { + + var model = this; + + model.$onDestroy = onDestroy; + model.$onInit = onInit; + + + function onDestroy() { + $element[0]._isInitializedUmbBlock = false; + } + + function onInit() { + $element[0]._isInitializedUmbBlock = true; + $scope.block = $element[0].$block; + $scope.api = $element[0].$api; + $scope.index = $element[0].$index; + $scope.culture = $element[0].$culture || null; + $scope.segment = $element[0].$segment || null; + $scope.parentForm = $element[0].$parentForm; + $scope.valFormManager = $element[0].$valFormManager; + + const stylesheet = $scope.block.config.stylesheet; + + var shadowRoot = $element[0].attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = + ` + + +
    + + +
    +
    + +
    + + + + + + +
    +
    +
    + `; + $compile(shadowRoot)($scope); + + } + + } + + var umbRteBlockComponent = { + bindings: { + dataUdi: "<" + }, + controller: umbRteBlockController, + controllerAs: "model" + }; + + angular.module('umbraco.directives').component('umbRteBlock', umbRteBlockComponent); + angular.module('umbraco.directives').component('umbRteBlockInline', umbRteBlockComponent); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js new file mode 100644 index 0000000000..4fe1beeb85 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js @@ -0,0 +1,955 @@ +(function () { + "use strict"; + + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbBlockListPropertyEditor + * @function + * + * @description + * The component for the block list property editor. + */ + angular + .module("umbraco") + .component("umbRtePropertyEditor", { + templateUrl: "views/propertyeditors/rte/umb-rte-property-editor.html", + controller: BlockRteController, + controllerAs: "vm", + bindings: { + model: "=" + }, + require: { + propertyForm: "^form", + umbProperty: "?^umbProperty", + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors', + umbElementEditorContent: '?^^umbElementEditorContent', + valFormManager: "^^valFormManager" + } + }); + + function BlockRteController($element, $scope, $q, $timeout, $interpolate, assetsService, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper, eventsService, $attrs, tinyMceAssets, tinyMceService) { + + var unsubscribe = []; + var modelObject; + + // Property actions: + //let copyAllBlocksAction = null; + //let deleteAllBlocksAction = null; + //let pasteSingleBlockAction = null; + + var liveEditing = true; + + var vm = this; + + vm.readonly = false; + vm.tinyMceEditor = null; + + $attrs.$observe('readonly', (value) => { + vm.readonly = value !== undefined; + + vm.blockEditorApi.readonly = vm.readonly; + + /*if (deleteAllBlocksAction) { + deleteAllBlocksAction.isDisabled = vm.readonly; + }*/ + }); + + vm.loading = true; + vm.rteLoading = true; + vm.blocksLoading = true; + vm.updateLoading = function () { + if(!vm.rteLoading && !vm.blocksLoading) { + vm.loading = false; + } + } + vm.currentBlockInFocus = null; + vm.setBlockFocus = function (block) { + if (vm.currentBlockInFocus !== null) { + vm.currentBlockInFocus.focus = false; + } + vm.currentBlockInFocus = block; + block.focus = true; + }; + + vm.supportCopy = clipboardService.isSupported(); + vm.clipboardItems = []; + unsubscribe.push(eventsService.on("clipboardService.storageUpdate", updateClipboard)); + unsubscribe.push($scope.$on("editors.content.splitViewChanged", (event, eventData) => { + var compositeId = vm.umbVariantContent.editor.compositeId; + if(eventData.editors.some(x => x.compositeId === compositeId)) { + updateAllBlockObjects(); + } + })); + + vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model. + vm.availableBlockTypes = []; // Available block entries of this property editor. + vm.labels = {}; + vm.options = { + createFlow: false + }; + + localizationService.localizeMany(["blockEditor_insertBlock", "content_createEmpty"]).then(function (data) { + vm.labels.blockEditor_insertBlock = data[0]; + vm.labels.content_createEmpty = data[1]; + }); + + vm.$onInit = function() { + + if (vm.umbProperty && !vm.umbVariantContent) {// if we dont have vm.umbProperty, it means we are in the DocumentTypeEditor. + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "umbVariantContentController"); + vm.umbVariantContent = found ? found.vm : null; + if (!vm.umbVariantContent) { + throw "Could not find umbVariantContent in the $scope chain"; + } + } + + // set the onValueChanged callback, this will tell us if the block list model changed on the server + // once the data is submitted. If so we need to re-initialize + vm.model.onValueChanged = onServerValueChanged; + liveEditing = vm.model.config.useLiveEditing; + + vm.listWrapperStyles = {}; + + if (vm.model.config.maxPropertyWidth) { + vm.listWrapperStyles['max-width'] = vm.model.config.maxPropertyWidth; + } + + // We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated. + ensurePropertyValue(vm.model.value); + + var scopeOfExistence = $scope; + if (vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) { + scopeOfExistence = vm.umbVariantContentEditors.getScope(); + } else if(vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) { + scopeOfExistence = vm.umbElementEditorContent.getScope(); + } + + /* + copyAllBlocksAction = { + labelKey: "clipboard_labelForCopyAllEntries", + labelTokens: [vm.model.label], + icon: "icon-documents", + method: requestCopyAllBlocks, + isDisabled: true, + useLegacyIcon: false + }; + + deleteAllBlocksAction = { + labelKey: "clipboard_labelForRemoveAllEntries", + labelTokens: [], + icon: "icon-trash", + method: requestDeleteAllBlocks, + isDisabled: true, + useLegacyIcon: false + }; + + var propertyActions = [copyAllBlocksAction, deleteAllBlocksAction]; + */ + + // Create Model Object, to manage our data for this Block Editor. + modelObject = blockEditorService.createModelObject(vm.model.value.blocks, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope); + const blockModelObjectLoading = modelObject.load() + blockModelObjectLoading.then(onLoaded); + + + // ******************** // + // RTE PART: + // ******************** // + + + // To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias + // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because + // we have this mini content editor panel that can be launched with MNTP. + vm.textAreaHtmlId = vm.model.alias + "_" + String.CreateGuid(); + + var editorConfig = vm.model.config ? vm.model.config.editor : null; + if (!editorConfig || Utilities.isString(editorConfig)) { + editorConfig = tinyMceService.defaultPrevalues(); + } + + var width = editorConfig.dimensions ? parseInt(editorConfig.dimensions.width, 10) || null : null; + var height = editorConfig.dimensions ? parseInt(editorConfig.dimensions.height, 10) || null : null; + + vm.containerWidth = "auto"; + vm.containerHeight = "auto"; + vm.containerOverflow = "inherit"; + + var promises = [blockModelObjectLoading]; + + //queue file loading + tinyMceAssets.forEach(function (tinyJsAsset) { + promises.push(assetsService.loadJs(tinyJsAsset, $scope)); + }); + + promises.push(tinyMceService.getTinyMceEditorConfig({ + htmlId: vm.textAreaHtmlId, + stylesheets: editorConfig.stylesheets, + toolbar: editorConfig.toolbar, + mode: editorConfig.mode + })); + + //wait for queue to end + $q.all(promises).then(function (result) { + + var standardConfig = result[promises.length - 1]; + + if (height !== null) { + standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1); + } + + //create a baseline Config to extend upon + var baseLineConfigObj = { + maxImageSize: editorConfig.maxImageSize, + width: width, + height: height + }; + + baseLineConfigObj.setup = function (editor) { + + //set the reference + vm.tinyMceEditor = editor; + + vm.tinyMceEditor.on('init', function (e) { + $timeout(function () { + vm.rteLoading = false; + vm.updateLoading(); + }); + }); + vm.tinyMceEditor.on("focus", function () { + $element[0].dispatchEvent(new CustomEvent('umb-rte-focus', {composed: true, bubbles: true})); + }); + vm.tinyMceEditor.on("blur", function () { + $element[0].dispatchEvent(new CustomEvent('umb-rte-blur', {composed: true, bubbles: true})); + }); + + //initialize the standard editor functionality for Umbraco + tinyMceService.initializeEditor({ + //scope: $scope, + editor: editor, + toolbar: editorConfig.toolbar, + model: vm.model, + getValue: function () { + return vm.model.value.markup; + }, + setValue: function (newVal) { + vm.model.value.markup = newVal; + $scope.$evalAsync(); + }, + culture: vm.umbProperty?.culture ?? null, + segment: vm.umbProperty?.segment ?? null, + blockEditorApi: vm.blockEditorApi, + parentForm: vm.propertyForm, + valFormManager: vm.valFormManager, + currentFormInput: $scope.rteForm.modelValue + }); + + }; + + Utilities.extend(baseLineConfigObj, standardConfig); + + // Readonly mode + baseLineConfigObj.toolbar = vm.readonly ? false : baseLineConfigObj.toolbar; + baseLineConfigObj.readonly = vm.readonly ? 1 : baseLineConfigObj.readonly; + + // We need to wait for DOM to have rendered before we can find the element by ID. + $timeout(function () { + tinymce.init(baseLineConfigObj); + }, 50); + + //listen for formSubmitting event (the result is callback used to remove the event subscription) + unsubscribe.push($scope.$on("formSubmitting", function () { + if (vm.tinyMceEditor != null && !vm.rteLoading) { + + // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. + var blockElements = vm.tinyMceEditor.dom.select(`umb-rte-block, umb-rte-block-inline`); + const usedContentUdis = blockElements.map(blockElement => blockElement.getAttribute('data-content-udi')); + + const unusedBlocks = vm.layout.filter(x => usedContentUdis.indexOf(x.contentUdi) === -1); + unusedBlocks.forEach(blockLayout => { + deleteBlock(blockLayout.$block); + }); + + + // Remove Angular Classes from markup: + var parser = new DOMParser(); + var doc = parser.parseFromString(vm.model.value.markup, 'text/html'); + + // Get all elements in the parsed document + var elements = doc.querySelectorAll('*[class]'); + elements.forEach(element => { + var classAttribute = element.getAttribute("class"); + if (classAttribute) { + // Split the class attribute by spaces and remove "ng-scope" and "ng-isolate-scope" + var classes = classAttribute.split(" "); + var newClasses = classes.filter(function (className) { + return className !== "ng-scope" && className !== "ng-isolate-scope"; + }); + + // Update the class attribute with the remaining classes + if (newClasses.length > 0) { + element.setAttribute('class', newClasses.join(' ')); + } else { + // If no remaining classes, remove the class attribute + element.removeAttribute('class'); + } + } + }); + + vm.model.value.markup = doc.body.innerHTML; + + } + })); + + vm.focusRTE = function () { + vm.tinyMceEditor.focus(); + } + + // When the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + $scope.$on('$destroy', function () { + if (vm.tinyMceEditor != null) { + if($element) { + $element[0]?.dispatchEvent(new CustomEvent('blur', {composed: true, bubbles: true})); + } + vm.tinyMceEditor.destroy(); + vm.tinyMceEditor = null; + } + }); + + }); + + }; + + // Called when we save the value, the server may return an updated data and our value is re-synced + // we need to deal with that here so that our model values are all in sync so we basically re-initialize. + function onServerValueChanged(newVal, oldVal) { + + ensurePropertyValue(newVal); + + modelObject.update(vm.model.value.blocks, $scope); + onLoaded(); + } + + function ensurePropertyValue(newVal) { + // We need to ensure that the property model value is an object, this is needed for modelObject to receive a reference and keep that updated. + if (typeof newVal !== 'object' || newVal == null) {// testing if we have null or undefined value or if the value is set to another type than Object. + vm.model.value = {markup:vm.model.value ?? "", blocks: {}}; + } else if(!newVal.markup) { + vm.model.value.markup = ""; + } else if(!newVal.blocks) { + vm.model.value.blocks = {}; + } + } + + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + } + + function onLoaded() { + + // Store a reference to the layout model, because we need to maintain this model. + vm.layout = modelObject.getLayout([]); + + var invalidLayoutItems = []; + + // Append the blockObjects to our layout. + vm.layout.forEach(entry => { + // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject. + if (entry.$block === undefined || entry.$block === null || entry.$block.data === undefined) { + var block = getBlockObject(entry); + + // If this entry was not supported by our property-editor it would return 'null'. + if (block !== null) { + entry.$block = block; + } + else { + // then we need to filter this out and also update the underlying model. This could happen if the data + // is invalid for some reason or the data structure has changed. + invalidLayoutItems.push(entry); + } + } else { + updateBlockObject(entry.$block); + } + }); + + // remove the ones that are invalid + invalidLayoutItems.forEach(entry => { + var index = vm.layout.findIndex(x => x === entry); + if (index >= 0) { + vm.layout.splice(index, 1); + } + }); + + vm.availableContentTypesAliases = modelObject.getAvailableAliasesForBlockContent(); + vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker(); + + updateClipboard(true); + + vm.blocksLoading = false; + vm.updateLoading(); + + $scope.$evalAsync(); + + } + + function updateAllBlockObjects() { + // Update the blockObjects in our layout. + vm.layout.forEach(entry => { + // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject. + if (entry.$block) { + updateBlockObject(entry.$block); + } + }); + } + + function getDefaultViewForBlock(block) { + + // TODO: new paths: + var defaultViewFolderPath = "views/propertyeditors/rte/blocks/blockrteentryeditors/"; + + if (block.config.unsupported === true) { + return defaultViewFolderPath + "unsupportedblock/unsupportedblock.editor.html"; + } + + return defaultViewFolderPath + "labelblock/rtelabelblock.editor.html"; + } + + /** + * Ensure that the containing content variant language and current property culture is transferred along + * to the scaffolded content object representing this block. + * This is required for validation along with ensuring that the umb-property inheritance is constantly maintained. + * @param {any} content + */ + function ensureCultureData(content) { + + if (!content) return; + + if (vm.umbVariantContent.editor.content.language) { + // set the scaffolded content's language to the language of the current editor + content.language = vm.umbVariantContent.editor.content.language; + } + // currently we only ever deal with invariant content for blocks so there's only one + content.variants[0].tabs.forEach(tab => { + tab.properties.forEach(prop => { + // set the scaffolded property to the culture of the containing property + prop.culture = vm.umbProperty.property.culture; + }); + }); + + // set the scaffolded allowed actions to the allowed actions of the document + content.allowedActions = vm.umbVariantContent.content.allowedActions; + + // set the scaffolded variants' allowed actions to the allowed actions of the current variant + content.variants.forEach(variant => { + variant.allowedActions = vm.umbVariantContent.editor.content.allowedActions; + }); + } + + function getBlockObject(entry) { + var block = modelObject.getBlockObject(entry); + + if (block === null) return null; + + block.view = (block.config.view ? block.config.view : getDefaultViewForBlock(block)); + block.showValidation = block.config.view ? true : false; + + block.hideContentInOverlay = block.config.forceHideContentEditorInOverlay === true; + block.showContent = !block.hideContentInOverlay && block.content?.variants[0].tabs?.some(tab=>tab.properties.length) === true; + block.showSettings = block.config.settingsElementTypeKey != null; + + // If we have content, otherwise it doesn't make sense to copy. + block.showCopy = vm.supportCopy && block.config.contentElementTypeKey != null; + + // Index is not begin updated in RTE Blocks, the order of element and Blocks of layout is not synced, meaning the index could be incorrect depending on the perspective. + block.index = 0; + block.setParentForm = function (parentForm) { + this._parentForm = parentForm; + }; + + /** decorator methods, to enable switching out methods without loosing references that would have been made in Block Views codes */ + block.activate = function() { + this._activate(); + }; + block.edit = function() { + this._edit(); + }; + block.editSettings = function() { + this._editSettings(); + }; + block.requestDelete = function() { + this._requestDelete(); + }; + block.delete = function() { + this._delete(); + }; + block.copy = function() { + this._copy(); + }; + updateBlockObject(block); + + return block; + } + + /** As the block object now contains references to this instance of a property editor, we need to ensure that the Block Object contains latest references. + * This is a bit hacky but the only way to maintain this reference currently. + * Notice this is most relevant for invariant properties on variant documents, specially for the scenario where the scope of the reference we stored is destroyed, therefor we need to ensure we always have references to a current running property editor*/ + function updateBlockObject(block) { + + ensureCultureData(block.content); + ensureCultureData(block.settings); + + block._activate = activateBlock.bind(null, block); + block._edit = function () { + var blockIndex = vm.layout.indexOf(this.layout); + editBlock(this, false, blockIndex, this._parentForm); + }; + block._editSettings = function () { + var blockIndex = vm.layout.indexOf(this.layout); + editBlock(this, true, blockIndex, this._parentForm); + }; + block._requestDelete = requestDeleteBlock.bind(null, block); + block._delete = deleteBlock.bind(null, block); + block._copy = copyBlock.bind(null, block); + } + + function addNewBlock(index, contentElementTypeKey) { + + // Create layout entry. (not added to property model jet.) + var layoutEntry = modelObject.create(contentElementTypeKey); + if (layoutEntry === null) { + return false; + } + + // make block model + var blockObject = getBlockObject(layoutEntry); + if (blockObject === null) { + return false; + } + + // If we reach this line, we are good to add the layoutEntry and blockObject to our models. + + // Add the Block Object to our layout entry. + layoutEntry.$block = blockObject; + + // add layout entry at the desired location in layout. + vm.layout.splice(index, 0, layoutEntry); + + // lets move focus to this new block. + vm.setBlockFocus(blockObject); + + setDirty(); + + return true; + } + + function deleteBlock(block) { + + var layoutIndex = vm.layout.findIndex(entry => entry.contentUdi === block.layout.contentUdi); + if (layoutIndex === -1) { + throw new Error("Could not find layout entry of block with udi: "+block.layout.contentUdi) + } + + setDirty(); + + var removed = vm.layout.splice(layoutIndex, 1); + removed.forEach(x => { + + var blockElementsOfThisUdi = vm.tinyMceEditor.dom.select(`umb-rte-block[data-content-udi='${x.contentUdi}'], umb-rte-block-inline[data-content-udi='${x.contentUdi}']`); + blockElementsOfThisUdi.forEach(blockElement => { + vm.tinyMceEditor.dom.remove(blockElement); + }); + + // remove any server validation errors associated + var guids = [udiService.getKey(x.contentUdi), (x.settingsUdi ? udiService.getKey(x.settingsUdi) : null)]; + guids.forEach(guid => { + if (guid) { + serverValidationManager.removePropertyError(guid, vm.umbProperty.property.culture, vm.umbProperty.property.segment, "", { matchType: "contains" }); + } + }) + }); + + if(removed.length > 0) { + vm.model.value.markup = vm.tinyMceEditor.getContent(); + $scope.$evalAsync(); + } + + modelObject.removeDataAndDestroyModel(block); + } + + /*function deleteAllBlocks() { + while(vm.layout.length) { + deleteBlock(vm.layout[0].$block); + }; + }*/ + + function activateBlock(blockObject) { + blockObject.active = true; + } + + function editBlock(blockObject, openSettings, blockIndex, parentForm, options) { + + options = options || vm.options; + + // this must be set + if (blockIndex === undefined) { + throw "blockIndex was not specified on call to editBlock"; + } + + var wasNotActiveBefore = blockObject.active !== true; + + // don't open the editor overlay if block has hidden its content editor in overlays and we are requesting to open content, not settings. + if (openSettings !== true && blockObject.hideContentInOverlay === true) { + return; + } + + // if requesting to open settings but we dont have settings then return. + if (openSettings === true && !blockObject.config.settingsElementTypeKey) { + return; + } + + activateBlock(blockObject); + + // make a clone to avoid editing model directly. + var blockContentClone = Utilities.copy(blockObject.content); + var blockSettingsClone = null; + + if (blockObject.config.settingsElementTypeKey) { + blockSettingsClone = Utilities.copy(blockObject.settings); + } + + var blockEditorModel = { + $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: parentForm || vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) + hideContent: blockObject.hideContentInOverlay, + openSettings: openSettings === true, + createFlow: options.createFlow === true, + liveEditing: liveEditing, + title: blockObject.label, + view: "views/common/infiniteeditors/blockeditor/blockeditor.html", + size: blockObject.config.editorSize || "medium", + hideSubmitButton: vm.readonly, + submit: function(blockEditorModel) { + + if (liveEditing === false) { + // transfer values when submitting in none-live-editing mode. + blockObject.retrieveValuesFrom(blockEditorModel.content, blockEditorModel.settings); + } + + setDirty(); + blockObject.active = false; + editorService.close(); + }, + close: function(blockEditorModel) { + if (blockEditorModel.createFlow) { + deleteBlock(blockObject); + } else { + if (liveEditing === true) { + // revert values when closing in live-editing mode. + blockObject.retrieveValuesFrom(blockContentClone, blockSettingsClone); + } + if (wasNotActiveBefore === true) { + blockObject.active = false; + } + } + editorService.close(); + } + }; + + if (liveEditing === true) { + blockEditorModel.content = blockObject.content; + blockEditorModel.settings = blockObject.settings; + } else { + blockEditorModel.content = blockContentClone; + blockEditorModel.settings = blockSettingsClone; + } + + // open property settings editor + editorService.open(blockEditorModel); + } + + vm.requestShowCreate = requestShowCreate; + function requestShowCreate(createIndex, mouseEvent) { + + if (vm.blockTypePicker) { + return; + } + + if (vm.availableBlockTypes.length === 1) { + var wasAdded = false; + var blockType = vm.availableBlockTypes[0]; + + wasAdded = addNewBlock(createIndex, blockType.blockConfigModel.contentElementTypeKey); + + if(wasAdded && !(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + userFlowWhenBlockWasCreated(createIndex); + } + } else { + showCreateDialog(createIndex); + } + + } + + vm.requestShowClipboard = requestShowClipboard; + function requestShowClipboard(createIndex) { + showCreateDialog(createIndex, true); + } + + vm.showCreateDialog = showCreateDialog; + function showCreateDialog(createIndex, openClipboard, addedCallback) { + + if (vm.blockTypePicker) { + return; + } + + if (vm.availableBlockTypes.length === 0) { + alert("No Blocks configured for this data-type"); + return; + } + + if(createIndex === undefined) { + createIndex = vm.layout.length - 1; + } + + var amountOfAvailableTypes = vm.availableBlockTypes.length; + var blockPickerModel = { + $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) + availableItems: vm.availableBlockTypes, + title: vm.labels.blockEditor_insertBlock, + openClipboard: openClipboard, + orderBy: "$index", + view: "views/common/infiniteeditors/blockpicker/blockpicker.html", + size: (amountOfAvailableTypes > 8 ? "medium" : "small"), + filter: (amountOfAvailableTypes > 8), + clickPasteItem: function(item, mouseEvent) { + if (Array.isArray(item.pasteData)) { + const BlocksThatGotPasted = []; + var indexIncrementor = 0; + item.pasteData.forEach(function (entry) { + const wasAdded = requestPasteFromClipboard(createIndex + indexIncrementor, entry, item.type) + if (wasAdded) { + const newBlock = vm.layout[createIndex + indexIncrementor].$block; + BlocksThatGotPasted.push(newBlock); + indexIncrementor++; + } + }); + if(BlocksThatGotPasted.length > 0) { + addedCallback(BlocksThatGotPasted); + } + } else { + const wasAdded = requestPasteFromClipboard(createIndex, item.pasteData, item.type); + if(wasAdded && vm.layout[createIndex]) { + const newBlock = vm.layout[createIndex].$block; + addedCallback(newBlock); + } + } + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + blockPickerModel.close(); + } + }, + submit: function(blockPickerModel, mouseEvent) { + var wasAdded = false; + if (blockPickerModel && blockPickerModel.selectedItem) { + wasAdded = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentElementTypeKey); + if(wasAdded && vm.layout[createIndex]) { + const newBlock = vm.layout[createIndex].$block; + addedCallback(newBlock); + } + } + + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + editorService.close(); + if (wasAdded) { + userFlowWhenBlockWasCreated(createIndex); + } + } + }, + close: function() { + // If opened by a inline creator button(index less than length), we want to move the focus away, to hide line-creator. + if (createIndex < vm.layout.length) { + vm.setBlockFocus(vm.layout[Math.max(createIndex-1, 0)].$block); + } + + editorService.close(); + } + }; + + blockPickerModel.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); + clipboardService.clearEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases); + }; + + blockPickerModel.clipboardItems = vm.clipboardItems; + + // open block picker overlay + editorService.open(blockPickerModel); + + }; + + function userFlowWhenBlockWasCreated(createIndex) { + if (vm.layout.length > createIndex) { + var blockObject = vm.layout[createIndex].$block; + if (blockObject.hideContentInOverlay !== true && blockObject.content.variants[0].tabs.find(tab => tab.properties.length > 0) !== undefined) { + vm.options.createFlow = true; + blockObject.edit(); + vm.options.createFlow = false; + } + } + } + + function updateClipboard(firstTime) { + + //var oldAmount = vm.clipboardItems.length; + + vm.clipboardItems = []; + + var entriesForPaste = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); + entriesForPaste.forEach(function (entry) { + var pasteEntry = { + type: clipboardService.TYPES.ELEMENT_TYPE, + date: entry.date, + pasteData: entry.data, + elementTypeModel: { + name: entry.label, + icon: entry.icon + } + } + if(Array.isArray(entry.data) === false) { + var scaffold = modelObject.getScaffoldFromAlias(entry.alias); + if(scaffold) { + pasteEntry.blockConfigModel = modelObject.getBlockConfiguration(scaffold.contentTypeKey); + } + } + vm.clipboardItems.push(pasteEntry); + }); + + entriesForPaste = clipboardService.retrieveEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases); + entriesForPaste.forEach(function (entry) { + var pasteEntry = { + type: clipboardService.TYPES.BLOCK, + date: entry.date, + pasteData: entry.data, + elementTypeModel: { + name: entry.label, + icon: entry.icon + } + } + if(Array.isArray(entry.data) === false) { + pasteEntry.blockConfigModel = modelObject.getBlockConfiguration(entry.data.data.contentTypeKey); + } + vm.clipboardItems.push(pasteEntry); + }); + + vm.clipboardItems.sort( (a, b) => { + return b.date - a.date + }); + + //pasteSingleBlockAction.isDisabled = vm.clipboardItems.length === 0; + } + + function copyBlock(block) { + clipboardService.copy(clipboardService.TYPES.BLOCK, block.content.contentTypeAlias, {"layout": block.layout, "data": block.data, "settingsData":block.settingsData}, block.label, block.content.icon, block.content.udi); + } + + function requestPasteFromClipboard(index, pasteEntry, pasteType) { + + if (pasteEntry === undefined) { + return false; + } + + var layoutEntry; + if (pasteType === clipboardService.TYPES.ELEMENT_TYPE) { + layoutEntry = modelObject.createFromElementType(pasteEntry); + } else if (pasteType === clipboardService.TYPES.BLOCK) { + layoutEntry = modelObject.createFromBlockData(pasteEntry); + } else { + // Not a supported paste type. + return false; + } + + if (layoutEntry === null) { + // Pasting did not go well. + return false; + } + + // make block model + var blockObject = getBlockObject(layoutEntry); + if (blockObject === null) { + // Initialization of the Block Object didn't go well, therefor we will fail the paste action. + return false; + } + + // set the BlockObject on our layout entry. + layoutEntry.$block = blockObject; + + // insert layout entry at the desired location in layout. + vm.layout.splice(index, 0, layoutEntry); + + vm.currentBlockInFocus = blockObject; + + return true; + } + + function requestDeleteBlock(block) { + if (vm.readonly) return; + + localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "contentTypeEditor_yesDelete"]).then(function (data) { + const overlay = { + title: data[0], + content: localizationService.tokenReplace(data[1], [block.label]), + submitButtonLabel: data[2], + close: function () { + overlayService.close(); + }, + submit: function () { + deleteBlock(block); + setDirty(); + overlayService.close(); + } + }; + + overlayService.confirmDelete(overlay); + }); + } + + function openSettingsForBlock(block, blockIndex, parentForm) { + editBlock(block, true, blockIndex, parentForm); + } + + function getBlockByContentUdi(blockContentUdi) { + + var layoutIndex = vm.layout.findIndex(entry => entry.contentUdi === blockContentUdi); + if (layoutIndex === -1) { + return undefined; + } + + return vm.layout[layoutIndex].$block; + } + + vm.blockEditorApi = { + getBlockByContentUdi: getBlockByContentUdi, + showCreateDialog: showCreateDialog, + activateBlock: activateBlock, + editBlock: editBlock, + copyBlock: copyBlock, + requestDeleteBlock: requestDeleteBlock, + deleteBlock: deleteBlock, + openSettingsForBlock: openSettingsForBlock, + readonly: vm.readonly, + singleBlockMode: false + }; + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js deleted file mode 100644 index 4973bede7a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ /dev/null @@ -1,133 +0,0 @@ -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.RTEController", - function ($scope, $q, assetsService, $timeout, tinyMceService, angularHelper, tinyMceAssets, $element) { - - // TODO: A lot of the code below should be shared between the grid rte and the normal rte - - var unsubscribe = []; - $scope.isLoading = true; - - //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias - // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because - // we have this mini content editor panel that can be launched with MNTP. - $scope.textAreaHtmlId = $scope.model.alias + "_" + String.CreateGuid(); - - var editorConfig = $scope.model.config ? $scope.model.config.editor : null; - if (!editorConfig || Utilities.isString(editorConfig)) { - editorConfig = tinyMceService.defaultPrevalues(); - } - - var width = editorConfig.dimensions ? parseInt(editorConfig.dimensions.width, 10) || null : null; - var height = editorConfig.dimensions ? parseInt(editorConfig.dimensions.height, 10) || null : null; - - $scope.containerWidth = "auto"; - $scope.containerHeight = "auto"; - $scope.containerOverflow = "inherit"; - - var promises = []; - - // we need to make sure that the element is initialized before we can init TinyMCE, because we find the placeholder by ID, so it needs to be appended to document before. - var initPromise = $q((resolve, reject) => { - this.$onInit = resolve; - }); - - promises.push(initPromise); - - //queue file loading - tinyMceAssets.forEach(function (tinyJsAsset) { - promises.push(assetsService.loadJs(tinyJsAsset, $scope)); - }); - - //stores a reference to the editor - var tinyMceEditor = null; - - promises.push(tinyMceService.getTinyMceEditorConfig({ - htmlId: $scope.textAreaHtmlId, - stylesheets: editorConfig.stylesheets, - toolbar: editorConfig.toolbar, - mode: editorConfig.mode - })); - - //wait for queue to end - $q.all(promises).then(function (result) { - - var standardConfig = result[promises.length - 1]; - - if (height !== null) { - standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1); - } - - //create a baseline Config to extend upon - var baseLineConfigObj = { - maxImageSize: editorConfig.maxImageSize, - width: width, - height: height - }; - - baseLineConfigObj.setup = function (editor) { - - //set the reference - tinyMceEditor = editor; - - tinyMceEditor.on('init', function (e) { - $timeout(function () { - $scope.isLoading = false; - }); - }); - tinyMceEditor.on("focus", function () { - $element[0].dispatchEvent(new CustomEvent('umb-rte-focus', {composed: true, bubbles: true})); - }); - tinyMceEditor.on("blur", function () { - $element[0].dispatchEvent(new CustomEvent('umb-rte-blur', {composed: true, bubbles: true})); - }); - - //initialize the standard editor functionality for Umbraco - tinyMceService.initializeEditor({ - editor: editor, - toolbar: editorConfig.toolbar, - model: $scope.model, - currentFormInput: $scope.rteForm.modelValue - }); - - }; - - Utilities.extend(baseLineConfigObj, standardConfig); - - // Readonly mode - baseLineConfigObj.toolbar = $scope.readonly ? false : baseLineConfigObj.toolbar; - baseLineConfigObj.readonly = $scope.readonly ? 1 : baseLineConfigObj.readonly; - - // We need to wait for DOM to have rendered before we can find the element by ID. - $timeout(function () { - tinymce.init(baseLineConfigObj); - }, 150); - - //listen for formSubmitting event (the result is callback used to remove the event subscription) - unsubscribe.push($scope.$on("formSubmitting", function () { - if (tinyMceEditor !== undefined && tinyMceEditor != null && !$scope.isLoading) { - $scope.model.value = tinyMceEditor.getContent(); - } - })); - - $scope.focus = function () { - tinyMceEditor.focus(); - } - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - $scope.$on('$destroy', function () { - for (var i = 0; i < unsubscribe.length; i++) { - unsubscribe[i](); - } - if (tinyMceEditor !== undefined && tinyMceEditor != null) { - if($element) { - $element[0]?.dispatchEvent(new CustomEvent('blur', {composed: true, bubbles: true})); - } - tinyMceEditor.destroy() - } - }); - - }); - - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html index d81b858002..94307f410e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html @@ -1,10 +1,4 @@ -
    - - - -
    - -
    -
    -
    -
    + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 1d872b6858..41055567e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -29,11 +29,11 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", tinyMceService.configuration().then(config => { $scope.tinyMceConfig = config; - + // extend commands with properties for font-icon and if it is a custom command $scope.tinyMceConfig.commands = _.map($scope.tinyMceConfig.commands, obj => { const icon = getIcon(obj.alias); - + const objCmd = Utilities.extend(obj, { fontIcon: icon.name, isCustom: icon.isCustom, @@ -63,7 +63,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", } }); }); - + }); stylesheetResource.getAll().then(stylesheets => { @@ -195,6 +195,10 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", icon.name = "icon-picture"; icon.isCustom = true; break; + case "umbblockpicker": + icon.name = "icon-document"; + icon.isCustom = true; + break; case "umbmacro": icon.name = "icon-settings-alt"; icon.isCustom = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/umb-rte-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/umb-rte-property-editor.html new file mode 100644 index 0000000000..2d7307676d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/umb-rte-property-editor.html @@ -0,0 +1,10 @@ +
    + + + +
    + +
    +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js new file mode 100644 index 0000000000..11f5debee9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js @@ -0,0 +1,39 @@ +(function () { + "use strict"; + + function WebhookLogController($q,$scope, webhooksResource, notificationsService, overlayService) { + var vm = this; + vm.logs = []; + vm.openLogOverlay = openLogOverlay; + vm.isChecked = isChecked; + + function loadLogs (){ + return webhooksResource.getLogs() + .then((data) => { + vm.logs = data.items; + }); + } + + function openLogOverlay (log) { + overlayService.open({ + view: "views/webhooks/overlays/details.html", + title: 'Details', + position: 'right', + log, + currentUser: this.currentUser, + close: () => { + overlayService.close(); + } + }); + } + + function isChecked (log) { + return log.statusCode === "OK"; + } + + loadLogs(); + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.WebhookLogController", WebhookLogController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html new file mode 100644 index 0000000000..10603d0ac1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html @@ -0,0 +1,47 @@ +
    + + + + + + + + + + + + + + + + + + + + + + +
    Webhook keyDateUrlEventRetryCount
    + + + + + {{ log.webhookKey}} + + {{ log.date}} + + {{ log.url }} + + {{ log.eventName }} + + {{ log.retryCount}} + + + +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html new file mode 100644 index 0000000000..9c0856cb67 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html @@ -0,0 +1,43 @@ +
    +
    +
    +
    + + +
    +
    {{model.webhookLogEntry.response.statusDescription}} ({{model.webhookLogEntry.response.statusCode}})
    +
    +
    +
    + Date +
    {{model.log.date}}
    +
    +
    + Url +
    {{model.log.url}}
    +
    +
    + Status Code +
    {{model.log.statusCode}}
    +
    +
    + Event +
    {{model.log.eventName}}
    +
    +
    + Retry count +
    {{model.log.retryCount}}
    +
    +
    + Request +
    {{model.log.requestHeaders}}
    +---
    +{{model.log.requestBody}}
    +
    +
    + Response +
    {{model.log.responseHeaders}}
    +---
    +{{model.log.responseBody}}
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js new file mode 100644 index 0000000000..c25ccaa6ba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js @@ -0,0 +1,121 @@ +(function () { + "use strict"; + + function EditController($scope, editorService, contentTypeResource, mediaTypeResource) { + var vm = this; + vm.clearContentType = clearContentType; + vm.clearEvent = clearEvent; + vm.removeHeader = removeHeader; + vm.openCreateHeader = openCreateHeader; + vm.openEventPicker = openEventPicker; + vm.openContentTypePicker = openContentTypePicker; + vm.close = close; + vm.submit = submit; + + + function openEventPicker() + { + editorService.eventPicker({ + title: "Select event", + selectedEvents: $scope.model.webhook.events, + submit(model) { + $scope.model.webhook.events = model.selection; + editorService.close(); + }, + close() { + editorService.close(); + } + }); + } + + function openContentTypePicker() + { + const isContent = $scope.model.webhook ? $scope.model.webhook.events[0].toLowerCase().includes("content") : null; + editorService.treePicker({ + section: 'settings', + treeAlias: isContent ? 'documentTypes' : 'mediaTypes', + entityType: isContent ? 'DocumentType' : 'MediaType', + multiPicker: true, + submit(model) { + getEntities(model.selection, isContent); + $scope.model.webhook.contentTypeKeys = model.selection.map((item) => item.key); + editorService.close(); + }, + close() { + editorService.close(); + } + }); + } + + function openCreateHeader() { + editorService.open({ + title: "Create header", + view: "views/webhooks/overlays/header.html", + size: 'small', + position: 'right', + submit(model) { + if (!$scope.model.webhook.headers) { + $scope.model.webhook.headers = {}; + } + $scope.model.webhook.headers[model.key] = model.value; + editorService.close(); + }, + close() { + editorService.close(); + } + }); + } + + function getEntities(selection, isContent) { + const resource = isContent ? contentTypeResource : mediaTypeResource; + $scope.model.contentTypes = []; + + selection.forEach((entity) => { + resource.getById(entity.key) + .then((data) => { + $scope.model.contentTypes.push(data); + }); + }); + } + + function clearContentType(contentTypeKey) { + if (Array.isArray($scope.model.webhook.contentTypeKeys)) { + $scope.model.webhook.contentTypeKeys = $scope.model.webhook.contentTypeKeys.filter(x => x !== contentTypeKey); + } + if (Array.isArray($scope.model.contentTypes)) { + $scope.model.contentTypes = $scope.model.contentTypes.filter(x => x.key !== contentTypeKey); + } + } + + function clearEvent(event) { + if (Array.isArray($scope.model.webhook.events)) { + $scope.model.webhook.events = $scope.model.webhook.events.filter(x => x !== event); + } + + if (Array.isArray($scope.model.contentTypes)) { + $scope.model.events = $scope.model.events.filter(x => x.key !== event); + } + } + + function removeHeader(key) { + delete $scope.model.webhook.headers[key]; + } + + + function close() + { + if ($scope.model.close) { + $scope.model.close(); + } + } + + function submit() + { + if ($scope.model.submit) { + $scope.model.submit($scope.model); + } + } + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.EditController", EditController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html new file mode 100644 index 0000000000..40e216c79a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html @@ -0,0 +1,141 @@ +
    + + + + + + + + +
    + + + + + + + + + Add + + + + + + + + Add + + Please select an event first. + + + + + + + + + + + + + + + + + + + + + +
    NameValue
    + {{ key }} + + {{ value }} + + + +
    + + +
    +
    +
    +
    +
    + + + + + + + + +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.controller.js new file mode 100644 index 0000000000..a77a4f5c00 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.controller.js @@ -0,0 +1,21 @@ +(function () { + "use strict"; + function HeaderController($scope) { + var vm = this; + $scope.headerModel = { key: "", value: "" }; + vm.submit = submit; + vm.close = close; + + function submit () { + if ($scope.headerModel.key && $scope.headerModel.value) { + $scope.model.submit($scope.headerModel); + } + } + + function close () { + $scope.model.close(); + } + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.HeaderController", HeaderController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.html new file mode 100644 index 0000000000..b9adcb2bf6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.html @@ -0,0 +1,61 @@ +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js new file mode 100644 index 0000000000..0dc8ebf5a8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js @@ -0,0 +1,64 @@ +(function () { + "use strict"; + + function OverviewController($q, $location, $routeParams, notificationsService, editorService, overlayService, localizationService) { + var vm = this; + vm.page = {}; + vm.page.labels = {}; + vm.page.name = ""; + vm.page.navigation = []; + let webhookUri = $routeParams.method; + + + onInit(); + + function onInit() { + + loadNavigation(); + + setPageName(); + } + + function loadNavigation() { + + var labels = ["treeHeaders_webhooks", "webhooks_logs"]; + + localizationService.localizeMany(labels).then(function (data) { + vm.page.labels.webhooks = data[0]; + vm.page.labels.logs = data[1]; + + vm.page.navigation = [ + { + "name": vm.page.labels.webhooks, + "icon": "icon-directions-alt", + "view": "views/webhooks/webhooks.html", + "active": webhookUri === 'overview', + "alias": "umbWebhooks", + "action": function () { + $location.path("/settings/webhooks/overview"); + } + }, + { + "name": vm.page.labels.logs, + "icon": "icon-box-alt", + "view": "views/webhooks/logs.html", + "active": webhookUri === 'logs', + "alias": "umbWebhookLogs", + "action": function () { + $location.path("/settings/webhooks/overview"); + } + } + ]; + }); + } + + function setPageName() { + localizationService.localize("treeHeaders_webhooks").then(function (data) { + vm.page.name = data; + }) + } + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.OverviewController", OverviewController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html new file mode 100644 index 0000000000..2576fa1e8b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html @@ -0,0 +1,23 @@ +
    + + + + + + + + + + + + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js new file mode 100644 index 0000000000..5814d6f6dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js @@ -0,0 +1,182 @@ +(function () { + "use strict"; + + function WebhookController($q,$scope, webhooksResource, notificationsService, editorService, overlayService, contentTypeResource, mediaTypeResource) { + var vm = this; + + vm.openWebhookOverlay = openWebhookOverlay; + vm.deleteWebhook = deleteWebhook; + vm.handleSubmissionError = handleSubmissionError; + vm.resolveTypeNames = resolveTypeNames; + vm.resolveEventNames = resolveEventNames; + + vm.page = {}; + vm.webhooks = []; + vm.events = []; + vm.webHooksContentTypes = {}; + vm.webhookEvents = {}; + + function loadEvents (){ + return webhooksResource.getAllEvents() + .then((data) => { + vm.events = data.map(item => item.eventName); + }); + } + + function resolveEventNames(webhook) { + webhook.events.forEach((event) => { + if (!vm.webhookEvents[webhook.key]) { + vm.webhookEvents[webhook.key] = event; + } else { + vm.webhookEvents[webhook.key] += ", " + event; + } + }); + } + + function getEntities(webhook) { + const isContent = webhook.events[0].toLowerCase().includes("content"); + const resource = isContent ? contentTypeResource : mediaTypeResource; + let entities = []; + + webhook.contentTypeKeys.forEach((key) => { + resource.getById(key) + .then((data) => { + entities.push(data); + }); + }); + + return entities; + } + + function resolveTypeNames(webhook) { + const isContent = webhook.events[0].toLowerCase().includes("content"); + const resource = isContent ? contentTypeResource : mediaTypeResource; + + if (vm.webHooksContentTypes[webhook.key]){ + delete vm.webHooksContentTypes[webhook.key]; + } + + webhook.contentTypeKeys.forEach((key) => { + resource.getById(key) + .then((data) => { + if (!vm.webHooksContentTypes[webhook.key]) { + vm.webHooksContentTypes[webhook.key] = data.name; + } else { + vm.webHooksContentTypes[webhook.key] += ", " + data.name; + } + }); + }); + } + + function handleSubmissionError (model, errorMessage) { + notificationsService.error(errorMessage); + model.disableSubmitButton = false; + model.submitButtonState = 'error'; + } + + function openWebhookOverlay (webhook) { + let isCreating = !webhook; + editorService.open({ + title: webhook ? 'Edit webhook' : 'Add webhook', + position: 'right', + size: 'small', + submitButtonLabel: webhook ? 'Save' : 'Create', + view: "views/webhooks/overlays/edit.html", + events: vm.events, + contentTypes : webhook ? getEntities(webhook) : null, + webhook: webhook ? webhook : {enabled: true}, + submit: (model) => { + model.disableSubmitButton = true; + model.submitButtonState = 'busy'; + if (!model.webhook.url) { + //Due to validation url will only be populated if it's valid, hence we can make do with checking url is there + handleSubmissionError(model, 'Please provide a valid URL. Did you include https:// ?'); + return; + } + if (!model.webhook.events || model.webhook.events.length === 0) { + handleSubmissionError(model, 'Please provide the event for which the webhook should trigger'); + return; + } + if(isCreating){ + webhooksResource.create(model.webhook) + .then(() => { + loadWebhooks() + notificationsService.success('Webhook saved.'); + editorService.close(); + }, x => { + let errorMessage = undefined; + if (x.data.ModelState) { + errorMessage = `Message: ${Object.values(x.data.ModelState).flat().join(' ')}`; + } + handleSubmissionError(model, `Error saving webhook. ${errorMessage ?? ''}`); + }); + } + else{ + webhooksResource.update(model.webhook) + .then(() => { + loadWebhooks() + notificationsService.success('Webhook saved.'); + editorService.close(); + }, x => { + let errorMessage = undefined; + if (x.data.ModelState) { + errorMessage = `Message: ${Object.values(x.data.ModelState).flat().join(' ')}`; + } + handleSubmissionError(model, `Error saving webhook. ${errorMessage ?? ''}`); + }); + } + + }, + close: () => { + editorService.close(); + } + }); + } + + function loadWebhooks(){ + webhooksResource + .getAll() + .then((result) => { + vm.webhooks = result; + vm.webhookEvents = {}; + vm.webHooksContentTypes = {}; + + vm.webhooks.forEach((webhook) => { + resolveTypeNames(webhook); + resolveEventNames(webhook); + }) + }); + } + + function deleteWebhook (webhook) { + overlayService.open({ + title: 'Confirm delete webhook', + content: 'Are you sure you want to delete the webhook?', + submitButtonLabel: 'Yes, delete', + submitButtonStyle: 'danger', + closeButtonLabel: 'Cancel', + submit: () => { + webhooksResource.delete(webhook.key) + .then(() => { + const index = this.webhooks.indexOf(webhook); + this.webhooks.splice(index, 1); + + notificationsService.success('Webhook deleted.'); + overlayService.close(); + }, () => { + notificationsService.error('Error deleting webhook.'); + }); + }, + close: () => { + overlayService.close(); + } + }); + } + + loadWebhooks() + loadEvents() + } + + angular.module("umbraco").controller("Umbraco.Editors.Webhooks.WebhookController", WebhookController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html new file mode 100644 index 0000000000..241a4a2015 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html @@ -0,0 +1,57 @@ +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    EnabledEventsUrlTypes
    + + + + + {{ vm.webhookEvents[webhook.key] }} + + {{ webhook.url }} + + {{ vm.webHooksContentTypes[webhook.key] }} + + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js index a835673e99..b60969b3d2 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js @@ -1,4 +1,4 @@ -describe('RTE controller tests', function () { +/*describe('RTE controller tests', function () { var scope, controllerFactory, element; //mock tinymce globals @@ -29,14 +29,15 @@ describe('RTE controller tests', function () { describe('initialization', function () { - it('should define the default properties on construction', function () { + it('should define the default properties on construction', function () { controllerFactory('Umbraco.PropertyEditors.RTEController', { $scope: scope, $routeParams: routeParams, $element: element }); }); - - }); -}); + }); + +}); +*/ diff --git a/src/Umbraco.Web.UI.Login/package-lock.json b/src/Umbraco.Web.UI.Login/package-lock.json index ea0b8cd21e..d7c296dd28 100644 --- a/src/Umbraco.Web.UI.Login/package-lock.json +++ b/src/Umbraco.Web.UI.Login/package-lock.json @@ -6,15 +6,15 @@ "": { "name": "login", "dependencies": { - "@umbraco-ui/uui": "^1.4.0", - "@umbraco-ui/uui-css": "^1.4.0", + "@umbraco-ui/uui": "^1.5.0", + "@umbraco-ui/uui-css": "^1.5.0", "lit": "^2.8.0", "msw": "^1.3.2", "rxjs": "^7.8.1" }, "devDependencies": { "typescript": "^5.2.2", - "vite": "^4.4.11" + "vite": "^4.5.0" }, "engines": { "node": ">=20.8", @@ -471,306 +471,153 @@ "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" }, "node_modules/@umbraco-ui/uui": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.4.0.tgz", - "integrity": "sha512-VG+C37WIS5Uv7ERDs/jQHT9mIncD9UrEsEQlgFnf2XZWc/TcBlV1Tvvt3xSYzZz9kIjwoymEG6lc5t6wJMqSfw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.5.0.tgz", + "integrity": "sha512-V9pAdCsiaBy+Vq23sZd9JJCk+TX6xMsclJtTUWhwCq8/YUh6KNERbdoVfMYGUZ1yyJ/g+yddQsWlYOxHNp8msw==", "dependencies": { - "@umbraco-ui/uui-action-bar": "1.4.0", - "@umbraco-ui/uui-avatar": "1.4.0", - "@umbraco-ui/uui-avatar-group": "1.4.0", - "@umbraco-ui/uui-badge": "1.4.0", - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-boolean-input": "1.4.0", - "@umbraco-ui/uui-box": "1.4.0", - "@umbraco-ui/uui-breadcrumbs": "1.4.0", - "@umbraco-ui/uui-button": "1.4.0", - "@umbraco-ui/uui-button-group": "1.4.0", - "@umbraco-ui/uui-button-inline-create": "1.4.0", - "@umbraco-ui/uui-card": "1.4.0", - "@umbraco-ui/uui-card-content-node": "1.4.0", - "@umbraco-ui/uui-card-media": "1.4.0", - "@umbraco-ui/uui-card-user": "1.4.0", - "@umbraco-ui/uui-caret": "1.4.0", - "@umbraco-ui/uui-checkbox": "1.4.0", - "@umbraco-ui/uui-color-area": "1.4.0", - "@umbraco-ui/uui-color-picker": "1.4.0", - "@umbraco-ui/uui-color-slider": "1.4.0", - "@umbraco-ui/uui-color-swatch": "1.4.0", - "@umbraco-ui/uui-color-swatches": "1.4.0", - "@umbraco-ui/uui-combobox": "1.4.0", - "@umbraco-ui/uui-combobox-list": "1.4.0", - "@umbraco-ui/uui-css": "1.4.0", - "@umbraco-ui/uui-dialog": "1.4.0", - "@umbraco-ui/uui-dialog-layout": "1.4.0", - "@umbraco-ui/uui-file-dropzone": "1.4.0", - "@umbraco-ui/uui-file-preview": "1.4.0", - "@umbraco-ui/uui-form": "1.4.0", - "@umbraco-ui/uui-form-layout-item": "1.4.0", - "@umbraco-ui/uui-form-validation-message": "1.4.0", - "@umbraco-ui/uui-icon": "1.4.0", - "@umbraco-ui/uui-icon-registry": "1.4.0", - "@umbraco-ui/uui-icon-registry-essential": "1.4.0", - "@umbraco-ui/uui-input": "1.4.0", - "@umbraco-ui/uui-input-file": "1.4.0", - "@umbraco-ui/uui-input-lock": "1.4.0", - "@umbraco-ui/uui-input-password": "1.4.0", - "@umbraco-ui/uui-keyboard-shortcut": "1.4.0", - "@umbraco-ui/uui-label": "1.4.0", - "@umbraco-ui/uui-loader": "1.4.0", - "@umbraco-ui/uui-loader-bar": "1.4.0", - "@umbraco-ui/uui-loader-circle": "1.4.0", - "@umbraco-ui/uui-menu-item": "1.4.0", - "@umbraco-ui/uui-modal": "1.4.0", - "@umbraco-ui/uui-pagination": "1.4.0", - "@umbraco-ui/uui-popover": "1.4.0", - "@umbraco-ui/uui-progress-bar": "1.4.0", - "@umbraco-ui/uui-radio": "1.4.0", - "@umbraco-ui/uui-range-slider": "1.4.0", - "@umbraco-ui/uui-ref": "1.4.0", - "@umbraco-ui/uui-ref-list": "1.4.0", - "@umbraco-ui/uui-ref-node": "1.4.0", - "@umbraco-ui/uui-ref-node-data-type": "1.4.0", - "@umbraco-ui/uui-ref-node-document-type": "1.4.0", - "@umbraco-ui/uui-ref-node-form": "1.4.0", - "@umbraco-ui/uui-ref-node-member": "1.4.0", - "@umbraco-ui/uui-ref-node-package": "1.4.0", - "@umbraco-ui/uui-ref-node-user": "1.4.0", - "@umbraco-ui/uui-scroll-container": "1.4.0", - "@umbraco-ui/uui-select": "1.4.0", - "@umbraco-ui/uui-slider": "1.4.0", - "@umbraco-ui/uui-symbol-expand": "1.4.0", - "@umbraco-ui/uui-symbol-file": "1.4.0", - "@umbraco-ui/uui-symbol-file-dropzone": "1.4.0", - "@umbraco-ui/uui-symbol-file-thumbnail": "1.4.0", - "@umbraco-ui/uui-symbol-folder": "1.4.0", - "@umbraco-ui/uui-symbol-lock": "1.4.0", - "@umbraco-ui/uui-symbol-more": "1.4.0", - "@umbraco-ui/uui-symbol-sort": "1.4.0", - "@umbraco-ui/uui-table": "1.4.0", - "@umbraco-ui/uui-tabs": "1.4.0", - "@umbraco-ui/uui-tag": "1.4.0", - "@umbraco-ui/uui-textarea": "1.4.0", - "@umbraco-ui/uui-toast-notification": "1.4.0", - "@umbraco-ui/uui-toast-notification-container": "1.4.0", - "@umbraco-ui/uui-toast-notification-layout": "1.4.0", - "@umbraco-ui/uui-toggle": "1.4.0" + "@umbraco-ui/uui-action-bar": "1.5.0", + "@umbraco-ui/uui-avatar": "1.5.0", + "@umbraco-ui/uui-avatar-group": "1.5.0", + "@umbraco-ui/uui-badge": "1.5.0", + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-boolean-input": "1.5.0", + "@umbraco-ui/uui-box": "1.5.0", + "@umbraco-ui/uui-breadcrumbs": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-button-group": "1.5.0", + "@umbraco-ui/uui-button-inline-create": "1.5.0", + "@umbraco-ui/uui-card": "1.5.0", + "@umbraco-ui/uui-card-content-node": "1.5.0", + "@umbraco-ui/uui-card-media": "1.5.0", + "@umbraco-ui/uui-card-user": "1.5.0", + "@umbraco-ui/uui-caret": "1.5.0", + "@umbraco-ui/uui-checkbox": "1.5.0", + "@umbraco-ui/uui-color-area": "1.5.0", + "@umbraco-ui/uui-color-picker": "1.5.0", + "@umbraco-ui/uui-color-slider": "1.5.0", + "@umbraco-ui/uui-color-swatch": "1.5.0", + "@umbraco-ui/uui-color-swatches": "1.5.0", + "@umbraco-ui/uui-combobox": "1.5.0", + "@umbraco-ui/uui-combobox-list": "1.5.0", + "@umbraco-ui/uui-css": "1.5.0", + "@umbraco-ui/uui-dialog": "1.5.0", + "@umbraco-ui/uui-dialog-layout": "1.5.0", + "@umbraco-ui/uui-file-dropzone": "1.5.0", + "@umbraco-ui/uui-file-preview": "1.5.0", + "@umbraco-ui/uui-form": "1.5.0", + "@umbraco-ui/uui-form-layout-item": "1.5.0", + "@umbraco-ui/uui-form-validation-message": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-icon-registry": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0", + "@umbraco-ui/uui-input": "1.5.0", + "@umbraco-ui/uui-input-file": "1.5.0", + "@umbraco-ui/uui-input-lock": "1.5.0", + "@umbraco-ui/uui-input-password": "1.5.0", + "@umbraco-ui/uui-keyboard-shortcut": "1.5.0", + "@umbraco-ui/uui-label": "1.5.0", + "@umbraco-ui/uui-loader": "1.5.0", + "@umbraco-ui/uui-loader-bar": "1.5.0", + "@umbraco-ui/uui-loader-circle": "1.5.0", + "@umbraco-ui/uui-menu-item": "1.5.0", + "@umbraco-ui/uui-modal": "1.5.0", + "@umbraco-ui/uui-pagination": "1.5.0", + "@umbraco-ui/uui-popover": "1.5.0", + "@umbraco-ui/uui-popover-container": "1.5.0", + "@umbraco-ui/uui-progress-bar": "1.5.0", + "@umbraco-ui/uui-radio": "1.5.0", + "@umbraco-ui/uui-range-slider": "1.5.0", + "@umbraco-ui/uui-ref": "1.5.0", + "@umbraco-ui/uui-ref-list": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0", + "@umbraco-ui/uui-ref-node-data-type": "1.5.0", + "@umbraco-ui/uui-ref-node-document-type": "1.5.0", + "@umbraco-ui/uui-ref-node-form": "1.5.0", + "@umbraco-ui/uui-ref-node-member": "1.5.0", + "@umbraco-ui/uui-ref-node-package": "1.5.0", + "@umbraco-ui/uui-ref-node-user": "1.5.0", + "@umbraco-ui/uui-scroll-container": "1.5.0", + "@umbraco-ui/uui-select": "1.5.0", + "@umbraco-ui/uui-slider": "1.5.0", + "@umbraco-ui/uui-symbol-expand": "1.5.0", + "@umbraco-ui/uui-symbol-file": "1.5.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.5.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.5.0", + "@umbraco-ui/uui-symbol-folder": "1.5.0", + "@umbraco-ui/uui-symbol-lock": "1.5.0", + "@umbraco-ui/uui-symbol-more": "1.5.0", + "@umbraco-ui/uui-symbol-sort": "1.5.0", + "@umbraco-ui/uui-table": "1.5.0", + "@umbraco-ui/uui-tabs": "1.5.0", + "@umbraco-ui/uui-tag": "1.5.0", + "@umbraco-ui/uui-textarea": "1.5.0", + "@umbraco-ui/uui-toast-notification": "1.5.0", + "@umbraco-ui/uui-toast-notification-container": "1.5.0", + "@umbraco-ui/uui-toast-notification-layout": "1.5.0", + "@umbraco-ui/uui-toggle": "1.5.0", + "@umbraco-ui/uui-visually-hidden": "1.5.0" } }, "node_modules/@umbraco-ui/uui-action-bar": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.4.0.tgz", - "integrity": "sha512-FMTSWXZOhWEziGL3OFvRGczAdRu2Ic82XLh4kCpCbRlKJHouqymOfo9FT3NbHEION37JUl9bv1nKiNA0m4s2bg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.5.0.tgz", + "integrity": "sha512-2B4ONNRTEtoKjnBo8mtvQo2Y9WW7LDSx6q85UuA+YEWfMOgZ0hr0lFepPg+qq/q90/8ZIoItoxRo16UFrPVaHQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-button-group": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button-group": "1.5.0" } }, "node_modules/@umbraco-ui/uui-avatar": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.4.0.tgz", - "integrity": "sha512-sUvQKsaWXP+5xQO5p2YAqQyUITiyzIzK6cVRlGRUoEla3QlhCd7YHrRnrIJTNxwmfPygDtxGa9Zx8GNkW8N91w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.5.0.tgz", + "integrity": "sha512-Iw4MQ2IMfJq590ydA6d2WXJ3gC7wO1vpA6tZj3T772B81LBZR31ftoMn3ho4cpavV5Nv4LvBnGhc2YajbsVn5A==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-avatar-group": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.4.0.tgz", - "integrity": "sha512-xpWMumABRNqVH3sdLBH43gBk8RSNjknTvqfuvfMgdrVUqAYE3cIjeadUDf9OfmzMWVoQn7PXyLSX7l/JRUhZJQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.5.0.tgz", + "integrity": "sha512-hlmqOGLQIN8uJMoLgT+RPHFWIxi8Ridhp/MrKgEjuNF6sTu4bCQyN28XuC9JD+4vBcSjU4a893QGvckalQxZiA==", "dependencies": { - "@umbraco-ui/uui-avatar": "1.4.0", - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-avatar": "1.5.0", + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-badge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.4.0.tgz", - "integrity": "sha512-6qUhcoGL43FWFS/Q6yozieaigQfKp2zqIrUGkdDpC3LqvUBshzuCFuDQEE+nobW/0oUkGV9MaMfa7hBI88eQTQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.5.0.tgz", + "integrity": "sha512-6azqqcqRzVHXYz/JfAody6kDZQG3hiBTiCS8EEYY9GcFNqh8BvFLX4yK9R6zz5BVrjgT3qkmPpE2iIpqV6J58A==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-base": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.4.0.tgz", - "integrity": "sha512-RcNY2WfE2vTyAiDVyItBdo/o5owgMF16V+IFqa4xHeFlu1i08fp9/Qmyk+5Mb4LRJatt/V21zaOM0QlloyuNUg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.5.0.tgz", + "integrity": "sha512-HzKRvbf/aPA1y8l9ZLTvF5Up7W6jX8UwqVUr1B8lwckI6tgxOEFPqLya+U4papqZDh4wz/lysXSDESeVfUy8cw==", "dependencies": { "lit": "^2.3.1" } }, "node_modules/@umbraco-ui/uui-boolean-input": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.4.0.tgz", - "integrity": "sha512-yIhvUpT5KBE+nmROtYdrkyTg7k5OQd2f5YpSKK2RrAA1Ex7J7ZZpGIO4B7w6wNuZLLPA657YxRADwrPKU91nNw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.5.0.tgz", + "integrity": "sha512-uhIPzi7n3Z4Li3n688Q8v3725apwasZvPntm7kMdtssXay6hUHOcor+hkpPavGXRVxZGg+9gIYRM6sQWp853cA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-box": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.4.0.tgz", - "integrity": "sha512-dQ8IeX86rAEmaz/ulJGDTGvmP0bMgm6LkRhGumignIRaVDLJdK5AIcPauVoq2n39IuczmoFjAEm6MFTAeQqZaQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.5.0.tgz", + "integrity": "sha512-uTHBvwzS9pRu0MVfN74+bux6lK0m1AmY/7xor9ez9/uzDyIK096D9jSLTQkfDyngIhqnV6kFLbG7PqcfQURFJQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", + "@umbraco-ui/uui-base": "1.5.0", "@umbraco-ui/uui-css": "1.4.0" } }, - "node_modules/@umbraco-ui/uui-breadcrumbs": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.4.0.tgz", - "integrity": "sha512-NfV8uVq093JceBC/Dog30iLi9z6ZwzwyS90At3qnCdIRn/ydxPghUA0xhS0Hf83GDQRgs9Ni7XbZv1P/SFdgrw==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-button": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.4.0.tgz", - "integrity": "sha512-8a6lZ/PLWg8iDuOv4YDhKvczWv844C3OfhPngLlmaK6UdkaiPlkxEoK41zZaVUV70B0ZhKk/odQYBp5nEUeeDA==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-icon-registry-essential": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-button-group": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.4.0.tgz", - "integrity": "sha512-Cwb1tFQbmo8XBpcTRwM5yolrselxBiDue0z+WyGWjKVuhNK/Cxlt1X2iT+MBlsgI1xW+I611+7d4n9V57wPXlQ==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-button-inline-create": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.4.0.tgz", - "integrity": "sha512-pngszZKSk4uIaW0L06aBjBImKykxarNp7JTx6YJqi+rF+GXTS31/gRuckWN4pN0/BgUTJMd0Q11zVWfB0uwjvA==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-card": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.4.0.tgz", - "integrity": "sha512-eS5QdKzNqQQ+en3ZpPq88YGSWD1mSr4Nk9okpZ06fQmEZlYMMliR0A3WKFBQHhnleZafaEgHq3VwpVL1SQrluw==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-card-content-node": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.4.0.tgz", - "integrity": "sha512-8xbaSytLMsA7pXMKI4gttgiXjRgoQFh/pc3HzaQf3hKaWfeCPUxUaponXfZXmXjqMAi+eoyyxS1qeUt+Zlt0Rw==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-card": "1.4.0", - "@umbraco-ui/uui-icon": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-card-media": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.4.0.tgz", - "integrity": "sha512-rQT4m0KFYMelEszFExFMYYNIBHHcYlDd0alqiKitEUBlpu2UXCHK7mXyQlU+sFWLJ262zSONMmwSaXsqhMLVug==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-card": "1.4.0", - "@umbraco-ui/uui-symbol-file": "1.4.0", - "@umbraco-ui/uui-symbol-folder": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-card-user": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.4.0.tgz", - "integrity": "sha512-t7C7F1sFrxAizNZJG7JDu+Wk0vizm7lN8UZCNggPiua6AkVVDpH8YN013Tk/reKxfTp9PkYh9aVUeAyyhWYa4g==", - "dependencies": { - "@umbraco-ui/uui-avatar": "1.4.0", - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-card": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-caret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.4.0.tgz", - "integrity": "sha512-RtWgCSvFelya+E0INy95XDiLNYDH3Tv7AdMvUTUKf/5PKYp/yR5MYo70P9EvUkCVMvIFVf/VVGd9mDwvLr2k+A==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-checkbox": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.4.0.tgz", - "integrity": "sha512-VCcYycChEPmaOo5q2QF1xsxxYQ5XToGh/z+46GmFyc5TDFP2OyOWqVm6+4gVpljcvf4aS9IRqcoONa/Bv2LQqQ==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-boolean-input": "1.4.0", - "@umbraco-ui/uui-icon-registry-essential": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-color-area": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.4.0.tgz", - "integrity": "sha512-csIswxLN9YDhmL6veZ9iR8SjQrDi8wscPPJB0i7w4TQDI8TwlvB0mAdb86FM0eoobXLPFeMDFkYGQijWpv69Gw==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "colord": "^2.9.3" - } - }, - "node_modules/@umbraco-ui/uui-color-picker": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.4.0.tgz", - "integrity": "sha512-zxOpmhEGEfQtLp/RYSPNBi8S2K+KjiuVyWhvmoqgO1gb/uNU5Om2xW1Q7pz/jiKe1qwWHO3whGl8LHM6el/C2w==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "colord": "^2.9.3" - } - }, - "node_modules/@umbraco-ui/uui-color-slider": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.4.0.tgz", - "integrity": "sha512-XEgi6shSGCnB4LhQgalcWfsHXyC2oLGw0ZCANr9l/4LpjaoZ0Uq4H/CL8UFfwiLXbJWdzZwqQqJcP928QmUFYQ==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-color-swatch": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.4.0.tgz", - "integrity": "sha512-/k1SgzfdA1sCueqDaGYXJyb+bZjMdffHgM4Qk5LMSjX3JDL+c6yKvoc/w2Bvky+9N1NUp+tEMbJKD7bzQalQlg==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-icon-registry-essential": "1.4.0", - "colord": "^2.9.3" - } - }, - "node_modules/@umbraco-ui/uui-color-swatches": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.4.0.tgz", - "integrity": "sha512-U6+0fu9OULPqRW0TuwVpj1PLectXM7ha2dc1Cw+rEzOtqBEbDmJTs4bh7EosMmxksmZQdXFhVkxu1yBHhXUJtQ==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-color-swatch": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-combobox": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.4.0.tgz", - "integrity": "sha512-epBlmRtVlUKeToA+DbYJYEWzTvKQahm2RnUMzFk9BvISP1xE9X5q7MtZLPRoiTjA9wf4SYrxIgHlYBGUOmy9lQ==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-button": "1.4.0", - "@umbraco-ui/uui-combobox-list": "1.4.0", - "@umbraco-ui/uui-icon": "1.4.0", - "@umbraco-ui/uui-scroll-container": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-combobox-list": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.4.0.tgz", - "integrity": "sha512-T6fOqHcOSB/NxfUmjZHlNWUU1ct9eVghXdQpA4tcPE83HSfHhWS5F1nbE9Cr/LO/al2Fe8iFfub9ed9OOsNqdA==", - "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" - } - }, - "node_modules/@umbraco-ui/uui-css": { + "node_modules/@umbraco-ui/uui-box/node_modules/@umbraco-ui/uui-css": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.4.0.tgz", "integrity": "sha512-HBCFPuXJijeZbjnjdqmg3oqOGB3RmpQKT/s/Uy0TSJfaQGfz0e73o2eRghYHWF2rdqHw6brKFrZTZHBVvCE/xA==", @@ -778,473 +625,679 @@ "lit": "^2.2.2" } }, - "node_modules/@umbraco-ui/uui-dialog": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.4.0.tgz", - "integrity": "sha512-FCrz17nKh2zybsDeN0AIxBQJjSFhK1q8OdZGSzaegPKx6R/xmZBPx6KPZeQnmjdGzQJHwh4xILKHXGazZbIZXA==", + "node_modules/@umbraco-ui/uui-breadcrumbs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.5.0.tgz", + "integrity": "sha512-mXuzt5o4NZ1E/HVTLYq+TklX9VQSH5zce+Ef1t2EgUE3EFQH0fwcdCRBC9SpklueNj46ngGHmVhyfv8ekne1Wg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-button": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.5.0.tgz", + "integrity": "sha512-ujicvfqUAN0JtBcgj8OG1YcyDaArTBdP5LvNsyYB8s0dePgcws71XzJ1mbHbXhuA386ioNue04yGDL+gSFlJ/A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-button-group": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.5.0.tgz", + "integrity": "sha512-8yhFdfg7p1B8MM2fIxIlc0Mmhnx46scdGhqeRhvaQ2/dcdpVTI1j1hI2JyOM18TUhJeot4olLqwatlXxlFFT+A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-button-inline-create": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.5.0.tgz", + "integrity": "sha512-J60vRf7nzQyRYKj+qYhMQR6LrQH6PyTrxyqyfDOVGzcWKzsTuRahxuVOIOzrs489cznwRYwL11jtK32MlrSjGQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-card": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.5.0.tgz", + "integrity": "sha512-RgpnQca3rpjMG/3DAmmrExI7gmNNHBNYwfjRqgCd/3QkBwRrtT/+jdppVsGRxxW5xAN90sJ/eLP7i3F5EfWlSA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-card-content-node": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.5.0.tgz", + "integrity": "sha512-aYGeTsppWT0KS9orrqkl9DF2v5l3gSGhBJZqIPiHVBOzczYIcgLWJbdAkaCgpwh1Zacbv3tnB/76965fd4EwPw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-card": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-card-media": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.5.0.tgz", + "integrity": "sha512-0KktT0IExh06W7QP1FMNqU+tpUL1qDwWeeA19PbZPXwHg15hbSW15a+Hc4aiwqlHYHOPT2gxXoiVc7jqWlMcSQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-card": "1.5.0", + "@umbraco-ui/uui-symbol-file": "1.5.0", + "@umbraco-ui/uui-symbol-folder": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-card-user": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.5.0.tgz", + "integrity": "sha512-xJjfkRHkt2xim1o+IvEPQiTpIQR+Z9+69096ssuGb3EkxyyUsDmH3aZZH6/+LKdtKR+7mPZVJub9TTWB4VRnwQ==", + "dependencies": { + "@umbraco-ui/uui-avatar": "1.5.0", + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-card": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-caret": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.5.0.tgz", + "integrity": "sha512-4Apw4TMALEydo5o31gsIyICuPVyKvG/oySNup+5psU3apS0JDQ1RXCgGVDFoFxt5xzM+iJ6/J8ZOOILMVNFM6Q==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-checkbox": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.5.0.tgz", + "integrity": "sha512-Kve+XAIkSFG9kowbZI1MpDEKihpMTtD9q36pcHiVENqxL1+Tydy60yjy3tHV8o6uamJ8qjR6ZlvLttRwLId9tQ==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-boolean-input": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-color-area": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.5.0.tgz", + "integrity": "sha512-FF6PrUCBo2nOg5iLbD+iB8aa3Vh+skIfqjFsPD80qLE0sKQ/53juZCnCbvvp7Z0YmIqwBlWP7xGEzJBGfS6OlA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "colord": "^2.9.3" + } + }, + "node_modules/@umbraco-ui/uui-color-picker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.5.0.tgz", + "integrity": "sha512-y/IwXhtaQJWNjwnZtYTvv47+bsmUYJzFLtXqxGckcUmyJQvoZ6DDxslTSv1B9J3QTXU0zpakqpxPszlNNHUygw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "colord": "^2.9.3" + } + }, + "node_modules/@umbraco-ui/uui-color-slider": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.5.0.tgz", + "integrity": "sha512-nkUpUxfD7VlayBHirM56xKqi1h0Opg7Q2suzxEC4KLDVLO1+L0KzsDORn1tfeantSG0PahBMbuve1XOoOwCrAA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-color-swatch": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.5.0.tgz", + "integrity": "sha512-UDqlGmJIMGyn7C23q33v8dkJoISmIAL0XZNTiPkEhwGjKRlxkbexmGd4L4vFt+nhJDRrN86JoZ64BRTHVN8V7A==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0", + "colord": "^2.9.3" + } + }, + "node_modules/@umbraco-ui/uui-color-swatches": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.5.0.tgz", + "integrity": "sha512-SvTKINbckKvqkkS4XnQfpELkW2x47CUa4PsnXqioXNIWP5sBJb9Kydiu0N1+lV57fAkteqNp+YY8mFxn3a6iPA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-color-swatch": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-combobox": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.5.0.tgz", + "integrity": "sha512-SoK4+yR0dJViXZinZ7iqowl6tvWPTTPSOBVE7FfOqOAgFoccOE/nQqjeNjSM0co80OKXqHUsh+kX/HwLjdyNEA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-combobox-list": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-scroll-container": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-combobox-list": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.5.0.tgz", + "integrity": "sha512-5cVlhnst3p6eEHFqn6O8LMswx3wdwpzlfAghleQJW+ZUIVo7ZPXznZz7+6yvnVWxnI7+xxFebHgC0KFxGMUVvg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-css": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.5.0.tgz", + "integrity": "sha512-jBSJg8KTWDG7DOVzz7A+UpMxMNHtddcLgt9k25vC4H+84xl+TN51RFTqF8C0JCZdWFK0eKWYlJsGqVrDfoVCcg==", + "dependencies": { + "lit": "^2.2.2" + } + }, + "node_modules/@umbraco-ui/uui-dialog": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.5.0.tgz", + "integrity": "sha512-m6J5i+eiLdNApryIY1KW/4kyunAuTpkcWBjQmxyESmlDIqRGdW0lqaahQvcZSZHto03jleUdH5wYTLNgKIb/rw==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", "@umbraco-ui/uui-css": "1.4.0" } }, "node_modules/@umbraco-ui/uui-dialog-layout": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.4.0.tgz", - "integrity": "sha512-67/yVhysc+wMsyVEQXSP2E21YlzoQfir/CQjxCRlfKGe8FdCck/m3HSnzyb1rvPfbXrxGUMCUmcTqDBoazBfAw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.5.0.tgz", + "integrity": "sha512-vfZ3FMzYccGBVvSSXvCeoHYX+VU8QppXtFR2OGDZwU0b8BOKtfKTP/2VLPEWCG4vJYKPmqZESo3N9bZXWDkWSg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-dialog/node_modules/@umbraco-ui/uui-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.4.0.tgz", + "integrity": "sha512-HBCFPuXJijeZbjnjdqmg3oqOGB3RmpQKT/s/Uy0TSJfaQGfz0e73o2eRghYHWF2rdqHw6brKFrZTZHBVvCE/xA==", + "dependencies": { + "lit": "^2.2.2" } }, "node_modules/@umbraco-ui/uui-file-dropzone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.4.0.tgz", - "integrity": "sha512-pbNcTS7x7fvSyCrvR+yA7HzjWLtJXLHcLZvkJ4yNoAxS1d4/5ppyi/Fyz0QakBgLWzPuBv1mKj2o6RvBy29QWA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.5.0.tgz", + "integrity": "sha512-3rkTWidY4k2fyktRxfsMVTSvF+EIguv9p1Fga7v4DCNkplCp6OyJnwWby5F//+NvTHphaGchxZirOWMLgLyDog==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-symbol-file-dropzone": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.5.0" } }, "node_modules/@umbraco-ui/uui-file-preview": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.4.0.tgz", - "integrity": "sha512-UYi4Omww0/COjheTuAUdvZHqEAITT65Vsi5NSDHaUH3AM9BSVlj0FR3wOpwF7OwbOXjIeIonMEC8xMf1JtjusQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.5.0.tgz", + "integrity": "sha512-Re+R8uZSD3t3jUgZvzG/DfQtihss7aw+rG41IAjmRO9wBZuUAsowfgCd2OJnuOYJXeaqOYYl+QQr7pmR2a/HNQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-symbol-file": "1.4.0", - "@umbraco-ui/uui-symbol-file-thumbnail": "1.4.0", - "@umbraco-ui/uui-symbol-folder": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-symbol-file": "1.5.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.5.0", + "@umbraco-ui/uui-symbol-folder": "1.5.0" } }, "node_modules/@umbraco-ui/uui-form": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.4.0.tgz", - "integrity": "sha512-jjukKI+eoKmvw9Jc8n0ryle6gAA1ogQM3GLgId509qS9qiFGxMetMJ0KQjcRkrisRM/oQjz7huf9tF1es/prOQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.5.0.tgz", + "integrity": "sha512-rbXFZzAg93/fzvNkxHavUr62DnSeWuVghd9CK9lhe6A9ER9cfjOcGn/INTYK3HHPBalay9IOq+WV1xxC5H6zyg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-form-layout-item": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.4.0.tgz", - "integrity": "sha512-aHBfwq7Y0YAWVHpiXZ1lnwSXyLbsGdk7lPkJ6hqVaBJ77VA/N2oDGMUjsRcCd1vKtD8AA3Nc2kT2e++NlUIPDg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.5.0.tgz", + "integrity": "sha512-owla3DWo1deVUEG0JzC7pE70h6Ll6lmbR+B+utbMdEgM6shEMdokpPioeCaXb8v7On9Whz+zJGAGBAYl/oyjug==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-form-validation-message": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-form-validation-message": "1.5.0" } }, "node_modules/@umbraco-ui/uui-form-validation-message": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.4.0.tgz", - "integrity": "sha512-AZXcvusVb48H5YrPIj71iMMUOXn2pZtensi3fUj55sVY1RNFa+QuJW/vC/79qDBLw/vQJu3NcZGbi4q4NBKh9A==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.5.0.tgz", + "integrity": "sha512-wuWCzttkUlEctqdJi9qzSzT8h10WvoK3+5usYB9V8NpdPYzOmbXU5RDYpoTWS0nPO56C6rlRlt3TH1khIQtPJA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-icon": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.4.0.tgz", - "integrity": "sha512-aLzVYbubk+VSI4iKHJSKFxlHMe9CGq5JbaUfuy9a9U/D7VfUUrroM+tDMPFP4qEvSkjthyCzdPBxodJ+QQOZew==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.5.0.tgz", + "integrity": "sha512-8Sz6PaYTC8KDCKj5ed+xnlnuh9/NOs0tQGPOma1bnVxGJN8LNjl+cJSLp+iU1m3Qq50H0TG+0K/dS3WUExjbZw==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-icon-registry": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.4.0.tgz", - "integrity": "sha512-76XXyxq96XIp4qIT58UgY4vp4+agD2YvfpCd+Dhs/rdu5iQq56PmYoxJ7qr7JYTSf8xxZ//0/PiuamwWkPmSEw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.5.0.tgz", + "integrity": "sha512-ei+HnaCKFjcCYjHYC0hqncY2vDfbgRkWhftOnrhqVZPJkE4omWDmVsLSGg/vm88ar1QleDmVj+CAa4J9T+uVeg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-icon": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0" } }, "node_modules/@umbraco-ui/uui-icon-registry-essential": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.4.0.tgz", - "integrity": "sha512-o1woHz7YFjyOBIQHsdoCxE3vpXrJ/Sj0QNcGexdlFqUsvv/LhHAJ9a88cmTve1Y8nYDWW2pyyKZbyX1nDokByg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.5.0.tgz", + "integrity": "sha512-nxNEQDI4SNBXnI2/Ov60vcdzKFyRCInwZDFNAKyt31F1yTNM0EM0ne5yV4AqM6YPOKVoWzqFcLz2rx64X+oLvQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-icon-registry": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon-registry": "1.5.0" } }, "node_modules/@umbraco-ui/uui-input": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.4.0.tgz", - "integrity": "sha512-mtlONZWmLV5OOYt2APhjl9cukTktrWNl1w4yF889F/wO2ZiGasBWwL9amtW4RIby/5nxns9yGgzXXG1/6GaqYw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.5.0.tgz", + "integrity": "sha512-TlbSIRh2Z7xJxW0GEPENd369W1hHgr9Y8IIRE5RDllXzZc8yho4QXPJSDFQTiHMf41LIkOTfIkrQst5047FiXg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-input-file": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.4.0.tgz", - "integrity": "sha512-qdRce6NA6VDgFR71hUhuasX28N4qmCtWscWwoU+2E/rxfYWd2MIFOSsBqnIW6R4wagw+LnC7YXV6oy4vZiCKuQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.5.0.tgz", + "integrity": "sha512-8h/qGED5KE7sb/YE7dHapZxcWXGm0qCPJft8AGOu/ZK/WdOUV1WHynLjV4yGVZgY9PVZGc+GQTzvdgwxxpltQw==", "dependencies": { - "@umbraco-ui/uui-action-bar": "1.4.0", - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-button": "1.4.0", - "@umbraco-ui/uui-file-dropzone": "1.4.0", - "@umbraco-ui/uui-icon": "1.4.0", - "@umbraco-ui/uui-icon-registry-essential": "1.4.0" + "@umbraco-ui/uui-action-bar": "1.5.0", + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-file-dropzone": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0" } }, "node_modules/@umbraco-ui/uui-input-lock": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.4.0.tgz", - "integrity": "sha512-LK9jgCmSJFENRA+Hj7qnwhuhuYmMgWYPc44LMYdowqTKlkffr67mY2VqaK+92WbjmH8PKStJr0wf0L8tuEczWQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.5.0.tgz", + "integrity": "sha512-KBhZLLD+5qyibbcp0AiJo7V4e/+GiKouGz/rCk6/3vxEKpe8CtWekcHhjrdlsHcOluQeBcb1Pdqng0wC9UTO5Q==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-button": "1.4.0", - "@umbraco-ui/uui-icon": "1.4.0", - "@umbraco-ui/uui-input": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-input": "1.5.0" } }, "node_modules/@umbraco-ui/uui-input-password": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.4.0.tgz", - "integrity": "sha512-slxRycyh8okgl6vH89O/y9lWPkfrga6s3Myijz4RXnprWfVtntIkB5pZoM17yT9bjSfo15UKd4E4GdOS9YpcaQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.5.0.tgz", + "integrity": "sha512-8wvQ/10jfufU0QWhK3gBVo5V/fzk4AuX8wPuieKZDY9Jnwkr7ugZ11DOJtaV3Az/4a0nrfF3TQ2gbBC7zHx2JA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-icon-registry-essential": "1.4.0", - "@umbraco-ui/uui-input": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0", + "@umbraco-ui/uui-input": "1.5.0" } }, "node_modules/@umbraco-ui/uui-keyboard-shortcut": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.4.0.tgz", - "integrity": "sha512-3hTFxrilMW7hGwfFtsNUmJdF0e4wk5pM8oGMuwwkKxsuxMdGzdpmht0PnB3G0EPQAsA60Xypiuvm0EgFnX91zg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.5.0.tgz", + "integrity": "sha512-KVTMHl6X0T4cUA3bUgM06xzwCN3VD5W3tZloF0i6e3PTHhkyCE5tKD/2Hizm56OGb+ifaI/oN3L1m7vEPC8IHw==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-label": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.4.0.tgz", - "integrity": "sha512-XTKH92Z0Apu15qI3MvJew1z3oAyOVBgByIipxVmWPb52Nlvj/Haa8QUlfksJWp4E4c2IhhYTPVXeft8CpS2q1w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.5.0.tgz", + "integrity": "sha512-Sc6XuMEyivBEQDfMOA6JT7nW5H4/eD6dzUtUNabOwzCG5GUpvTMfRccpdjmzOvl9VCGNWtE9ikqCBZWexWA6YA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-loader": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.4.0.tgz", - "integrity": "sha512-5KgUdzusuJeMwgIwtScuqgMnJ9NW+/G0/Osj3B20UBPwcwVm1z4s3cWlt3kKJmPA/W4fzbdTxRt2MRdSEp3+cw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.5.0.tgz", + "integrity": "sha512-lhl1KqRbM5NTp08fvxgzOsbHFz04z8/WjaOar6lqNnL0R+CcFtVWQrv69Opht9Sj1NdHESmHEVnX0yodod2LhQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-loader-bar": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.4.0.tgz", - "integrity": "sha512-n+sxqJxp1aKy7lF8rbB9a72OzcdhTuHif5bR2XD2NwMmEZ7jl6xd+Em+sHo35ePqqmualpwetM2DlO50/uTDgg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.5.0.tgz", + "integrity": "sha512-qUcVXi4i+ClozPc0Vfw7g90CLAQVj04F71xtatxDY5nhSWDEMEI6b/pXtN/B9TklkqfgE1mf/gRziFrpbVjLhA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-loader-circle": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.4.0.tgz", - "integrity": "sha512-mFQB6psm9W4U/g9KEPPoUNFeEju2k/oJ+J5I1g0fz20HpfvDKIoebqErcCd0wngZfk4FZm1ditpN3t2eFGBR4A==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.5.0.tgz", + "integrity": "sha512-059/DJDYbgOmr/LPXbiDaTkBcInmzUUu/YDtQt/SkZPCO33uuB7TDc+++cMgFYskdXBpqesNvVfZOUd4P6zJyA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-menu-item": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.4.0.tgz", - "integrity": "sha512-GqrjrUlzQzbctDzzg1X0fVRO4Yxll/H5oqnXZBuDZBpqu++AlXknqMuAjur0cFkeiV0Kn7N9w+uZl1NYWW9OJA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.5.0.tgz", + "integrity": "sha512-rmKuTz0Xgf0LyQRqs3tr2Z4O6oaNCd7UmI8kEbluk4yKpk5MU38BlFY9p39fpiEVUuzjcg9pBjrEyxrC/H9xjA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-loader-bar": "1.4.0", - "@umbraco-ui/uui-symbol-expand": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-loader-bar": "1.5.0", + "@umbraco-ui/uui-symbol-expand": "1.5.0" } }, "node_modules/@umbraco-ui/uui-modal": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.4.0.tgz", - "integrity": "sha512-v+jiYIGCLTL4NY+Td5UIgoK52pxGVWxWEe+xxNJLYSUtiRsp+7dw9UwmNqLdflR3ngfyBVY+rfEXSfbcfjiQdA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.5.0.tgz", + "integrity": "sha512-q9g4rA8OYCPlOmZMES/O17NiAu18wtMxNHMuT6dADP2tuULE+TKT6A8vqC7aq8JkWOTAXRAFvTjTmcvm6L2pvg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-pagination": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.4.0.tgz", - "integrity": "sha512-f38AuUTyZ5/JNWZFU02EzAaQ81R3sa38jClSjyDScQ9Vh+8Uwj16sRPnbnveFWU/c5URVMFpG5OGXA/RXI3WEg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.5.0.tgz", + "integrity": "sha512-I3gCWbyLRFvi5fAlezQZarvj7FuEZ7NVZbbKJxqEhbo1bwOxDMXlDNxIIrxSg3R8YAuDNP9Pbdw+rnQwupuOMQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-button": "1.4.0", - "@umbraco-ui/uui-button-group": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-button-group": "1.5.0" } }, "node_modules/@umbraco-ui/uui-popover": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.4.0.tgz", - "integrity": "sha512-jbYHUGoN1S81VU4TbUh0HKipGcCnqiwINtQNDGf2W61Rgy++wBR3MfWqCaXd1K10GL8+wgkly6RsJKKUzqrDNw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.5.0.tgz", + "integrity": "sha512-Ab8UL4UGxTUn6hYbTqPrMtyGpQr3Xw1E/PVKG3+j+UrNw1Ro5piKgh0TahwxLnrsXWOPXfy53oaXNYsMGenndA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-popover-container": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover-container/-/uui-popover-container-1.5.0.tgz", + "integrity": "sha512-issjf86TwvwLA6sJOs5pLRMFY+WBc4oeTZiJMz5mhZ5C5UoRmU65L6RP/0UnzZ4ZGY2Gpdh2YatNnZ7hVMg5ig==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-progress-bar": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.4.0.tgz", - "integrity": "sha512-lJdoxiJMaDl7Qsaa6TkeuiudWV7Zer1LjWS9yO0aAZ4xWkkVxLf89qIlaTukdOat+Sr8ZtI2mjmRih5IjMdalg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.5.0.tgz", + "integrity": "sha512-B/v7VsBBwo19Y+4NBRllt7Ls+WLQfx6vY57rfO8MQG7zxGznxpTSIYvd3wxdRuDsFQeVwwoYjF1/YBJ7iWUnEQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-radio": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.4.0.tgz", - "integrity": "sha512-pIJjmzWRIKPDxzwmB4CbBJNmMhlB97NOcgMoiIruiacVGEfZTWqXYXAkNtMragYGVQ0oz+ySYxEgl4iVvg2tdw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.5.0.tgz", + "integrity": "sha512-3e52VZHcgHB/17eLTmiZwdm7ENgfX6AF4Dw+8H2x8jdRjyvt8lbykCq+6xewAZFsLAu7vTOEKtd2RhQFI2+hwg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-range-slider": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.4.0.tgz", - "integrity": "sha512-3rGrXEAOfztQHvD8aJlGuBfe0tXkpZgWtzq888D+8X68RMvPHs89X32FVqT8e34kK1/vfm8I7BwbDSXL6FTzbg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.5.0.tgz", + "integrity": "sha512-oHmIoF+KrHDWiOKonIWq7n94C6CzStBXrleS6iwCgWY++ayaHKCPlCuQIYp3BmGjnMQn8Ou0r2x/RuBPuraLVQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.4.0.tgz", - "integrity": "sha512-zESd9N+72zON+kLCv95zzQtfmFY10zJU9DzzLR0GdZouujtyysU5qIwJG+dTy5ewm1jzGq5DHAyJtwO6IQSx7Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.5.0.tgz", + "integrity": "sha512-wba/OP6b/mG5kp4bUgBBcBAAy3RWTbokVyjb52FR7nyqNMnIE/UBdgi0XeBx4j6lZeEbr5k5ZOGQ1knEHbPWyQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref-list": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.4.0.tgz", - "integrity": "sha512-r8X0dSUsbvbyvK+2Yy7jsaCE4Q+PV7CDGQAO1eArYywCuJWjdVO18zt26Abvl1Z+v5qAWnbPiJHvF0h6mYTGMg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.5.0.tgz", + "integrity": "sha512-sxs3hC97zDuFaV8mvXLAbqqtWk0kqDdHY9ORt9CxacdT36nQS58Sw60/plCryqoyp7P2cUZVtlEeff53OKOTCQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref-node": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.4.0.tgz", - "integrity": "sha512-Jc8ews6mC9au4gUvzjRYfTeQWgFkrSICcsxd1oPz1qxVsyXWk66b3tWjAwkyjWwI13EOp4YoGK9QsPXbQKeTvg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.5.0.tgz", + "integrity": "sha512-bjmMgrIW+/4bmUXwMwFFaPrg2MeTxXssb6EpbBItJ+s0QhTEcTNyAD/DK3RlSMRE5VPO11sRwgCr06aIhklx0Q==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-icon": "1.4.0", - "@umbraco-ui/uui-ref": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-ref": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref-node-data-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.4.0.tgz", - "integrity": "sha512-tcuRnbYJxV8X3/ezP1gQ/DY2Vy9f+TDB/HFKtsNp+n891zShRbcEQ1As/fOoXGtM2JVAJ7VUYboyMhJ195hBVw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.5.0.tgz", + "integrity": "sha512-k14MI3cRELOmAwmtFeBzgCFw4+uin0JSqf85ZaqNkXSAmg+4I0ayUI6PGz+Jw66yGHvw3YNeUMKPmLO8l6M79A==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-ref-node": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref-node-document-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.4.0.tgz", - "integrity": "sha512-pWESJsEm+Dect5kUws+sse0Xj8Z9+ZZkR1ZaeTHDL3kPMLxD6wMfMwWJtMeAIh7OvqJY0B/ldLonTof/ysebdA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.5.0.tgz", + "integrity": "sha512-ouytDUaSls7Hsd0WaDy4wgfKMLpxlxx16WWyHlzX5lMyhkR+S3olyNZcgDRtz9xIQV+dVE3iDsUeQcNAigCdaw==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-ref-node": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref-node-form": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.4.0.tgz", - "integrity": "sha512-Xd17jQycvjq5TGfxkTZr+Kb/OU/lsUPkh4ft8/V4W/p0xv4sTio6txPw0bjDDcjJ/75zuHOLyTYicmcchcjXbA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.5.0.tgz", + "integrity": "sha512-D86A1+ScVGTer2kci6Y9X4ZAhCnm4kxUi7bCFH7dn7oi/Fq8fhs3PBuA7mr1FrZgrPvXVdW+Qa7ldxxU58NIWA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-ref-node": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref-node-member": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.4.0.tgz", - "integrity": "sha512-Lhpsh1CAwQRKOaR4tPkXBBZN3fjuEJMENlVHDB2UmmSJvFozl2byEWX4dEHwvPQpe0cbU8lE0By8iNDaEbl7Qw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.5.0.tgz", + "integrity": "sha512-/UPmUNk6KP2unKnJKjr1qGkdPlFGTRj3K7H/mczCY7IbtzEccdEswWJCdUy/doIkAKbDdaqKe3/9HBoA3JtWPw==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-ref-node": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref-node-package": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.4.0.tgz", - "integrity": "sha512-FQgAZ8NOjBVUWLyDg93pg6bqgONcM275qbqM3Htd+JMmmYcoYii/oTXlBqhGq7+9eDhcb8tGko2RN/tH9p8KSQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.5.0.tgz", + "integrity": "sha512-XkET8XKb3XxmjlIDrmtwm9o0QsaG81bcpUBEBA/wUC0OcJNrjTKyv6ciAVDP7HaW6XpN8XwsRbqdcrYwM8lXDQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-ref-node": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" } }, "node_modules/@umbraco-ui/uui-ref-node-user": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.4.0.tgz", - "integrity": "sha512-dmp44LDXJbnupP8dnUpAMSPCU2+udhMSE9uQDx1hfmX08Q49Phw6R4Az9h1ESh5uSxSm6UEb/Y7JEblods7C3w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.5.0.tgz", + "integrity": "sha512-9TrIr1JWw3cIkWfQrdv9iLRIqm/dd10d6uZEWaGJ/MuxyCywqMg/LSApV/NLapB4HXhIG4pGCiXvUa8OVW99ew==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-ref-node": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-ref-node": "1.5.0" } }, "node_modules/@umbraco-ui/uui-scroll-container": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.4.0.tgz", - "integrity": "sha512-/Rfqjtw+9LCCjvxl/MEmAjVfn4+aE8elfZ77EoItbF79R8WVmoJsIJUezjFp/Hvtp51PsgVgu/Da94dxTR4QBA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.5.0.tgz", + "integrity": "sha512-Xj5jnmCEDyRENmWtuPI1QYEMzrmi/9/LaajkPEIZEYVu2owI940F0viS5X+X/FvKehSxoSt9ainCwkLphgzNiw==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-select": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.4.0.tgz", - "integrity": "sha512-bvdVIGot2vWiuoQmQL9dCriY8KnmpqLyn0q6FCvx7xGAl9nFBn1MfZFbs4INxriIGWjq17YFvUXklTWuhMLGTA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.5.0.tgz", + "integrity": "sha512-lcMiIM6WxF5YraIXAqSpujx3OJzq6Snfik0BUypTWbUZdKVQTgLPh3A6We9PdD6K64AX2Zk4eH8yhQ+5GNImzQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-slider": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.4.0.tgz", - "integrity": "sha512-eDTIcXhYAiMSpPwI5e5gnMMpr0zOpx8te8pxF6K2YrGo8mCO2CI1zXZTzuv7e4ImL4HLmLoph8kbk+/wlrEtLw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.5.0.tgz", + "integrity": "sha512-Mp6xz7C7GbAuQ1Totd2WLzvS56ekx4l31mAvUvor0GqrUF/hHxwfrGZOAWoBqoTdKQAFKbZVSM782a+cwNv3hg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-symbol-expand": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.4.0.tgz", - "integrity": "sha512-vSWRYiUwTjERuWtbiAW7IB49s57bqjN2XrSmCrOtyS9i4t5jIjsZ11If97WD+gQI/tt+khQZ85oPWNcj6C3eVQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.5.0.tgz", + "integrity": "sha512-ZCuGAJT2qFs4wQ6Z+g/qV3obv/SbriMnaIOGy6XTTAuMlh2+aNAwm33Je0wYKCTwHNUmnl427wTMEkQcMziD4g==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-symbol-file": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.4.0.tgz", - "integrity": "sha512-wGmdw47jXjIcjpThf/TZ+6EZh+aQwqBA/1SMlgTtNBbUZDSy77NZ0pOWw8SaXzKqRrDqgFqIZukb7MILio3fwQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.5.0.tgz", + "integrity": "sha512-ClB/lT/ebyUBmPqExB2ZinMOo/bCMEgjGxjkXy2THX4lOLUqvjDNEKLq99MAREKSh/mmGq7iB3Z/hd9/EDu75Q==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-symbol-file-dropzone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.4.0.tgz", - "integrity": "sha512-GftR6cK+9kbY43fV9a3+ICJX0rn8iT6SEe9vt85Uu4JMi2GCOT3TnKnIxgXRP3u9SyHhMNMiWmSgRfLpgJ2v/w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.5.0.tgz", + "integrity": "sha512-0YL88rFFI5SOzzORtm1VtMihN4if7r0CIRe5Q3Sv0WwHjrMfIM08DeONCgN2j+ZoKgnTvt9KpE1OGigshouRug==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-symbol-file-thumbnail": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.4.0.tgz", - "integrity": "sha512-Xlu7NQ88AiQI9kfKOQKi1kH0zMkop7GqtGyuIXbnt7rM3EZfioTdltW1NvqgKzc2QpZPqMY1s449hravObHUUg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.5.0.tgz", + "integrity": "sha512-/qkf6AdAIsRmUfsBdtFkFk5wPWw6JvSVHvgk/UvZulHHb2F8TamPSJfb6voh86Vq8DzVIcy3ZbqatxH7LZBY1g==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-symbol-folder": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.4.0.tgz", - "integrity": "sha512-/cpV6Br3bOZkOh6YNr5PbIA/+NKKjyj1PkJwITSGm5/TnW2a4J5nzJTVn5ez7IjId176loRDZM2w05bemRavmA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.5.0.tgz", + "integrity": "sha512-Sxt4n5IBT+XIqu2nJxP4RnhourwC+1X5bD40YgUBmqZJ9KV//tox4zo2elU19WCeRZFkklZGfn2smLY1FD0OGg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-symbol-lock": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.4.0.tgz", - "integrity": "sha512-BUPxOwhjjl4GVixbbGkKOPi9FI+C1fr1cy5NT2uLNY64z5r3jFzbnHMySKGzvpfig8wD+1hsuSPGP3lypzknOQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.5.0.tgz", + "integrity": "sha512-EH7tEPCB+PTyjWbW+bdekk4M5hcjvYYpCKTnl3Pdpzh0mrxHPt9xa8908JB0tG8n0m0EcP+L7k8pthUmkgpK7A==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-symbol-more": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.4.0.tgz", - "integrity": "sha512-fx125CCeBY+sspQpWITYt79AKYZ11NFaa72Zquz8cxH+hQA1z32jOUDL+m6oF3jTYwQkKQlCoff3VnOaJ91VyA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.5.0.tgz", + "integrity": "sha512-EuhU4kle4swMFZnsguWPz77rOtrk0IQcXuEA60fjzFGJCwsg7yyu9Ns209IEUsYh5ktstj8pXKT8+ZDila5umg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-symbol-sort": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.4.0.tgz", - "integrity": "sha512-pVeT7qrKhRK8NUX3IDodSK0GNAKOKyWyzRhrxKrDT7wRuMManKmAK6WAVYpLaRqO+PRF8+NljfoCOEtJAHlGUg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.5.0.tgz", + "integrity": "sha512-/cifoZXuZbDmuZFPD0rr95Gpuy18DnboOYb/Ir6G3PANJ0fWOhzykHUrdx18ItLzhhwfE3dcZk4EWcGrEkfnfg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-table": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.4.0.tgz", - "integrity": "sha512-wpEqTmUQrAWjloeHZQqzAt5HR+j5ihMJusHpqZmY4076LcvnmpZHPhtmwpIzosZNqRq2N1rbrPIyEotlzSg9Fw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.5.0.tgz", + "integrity": "sha512-tjhpEzBYCQdgieoXcIgcOjROrScF0Ifutz/6gmpcdrXYbgZ+YkWX7dSLAeQj3fzGebaPbNYzGOmGZA9/opZ1rg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-tabs": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.4.0.tgz", - "integrity": "sha512-RWoLJHwMb9MbKqMyuyz3DaSc9ZGCa/NBtgBDpKpn/8oolbmNYBnr9e4sabHARtqfsEWFWKWP3kUw9iTQZNa0oA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.5.0.tgz", + "integrity": "sha512-0D5NLufis9Tzc5Vr+fl8Z0wABHyz1Tep76Qnx0nXyYzAZvdNq2IxThHbGqA1cb+FjVJSKdfp6ONfiPc/SIVAzA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", + "@umbraco-ui/uui-popover-container": "1.5.0", + "@umbraco-ui/uui-symbol-more": "1.5.0" } }, "node_modules/@umbraco-ui/uui-tag": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.4.0.tgz", - "integrity": "sha512-9R+WJrJav780ZoA+dbZb7bHYazxrHxADnLdNOHoLvNyggLyxIT/SRsSxrP3x9zFRwbcRLZ8MRxQ3I32YiWacKw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.5.0.tgz", + "integrity": "sha512-OZGitHjdn4coj1x7F7zfeIx5M9NhGd8+CqpD915V9Qm8YlTQxFLq1M8tqjIxaYAB5EcHXuyzRpSUCrt/WUvipA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-textarea": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.4.0.tgz", - "integrity": "sha512-nd6kWBmAvWaNLmXbEhfLRnWMfAp8rkll7XtHec9W32EQJwcHlYrS3wga6Xu32d3rKb3zUg+VXHh3EKKQH8M4uQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.5.0.tgz", + "integrity": "sha512-+zDqbYKYfaiG0IXEaQatUaWsD4umtkTtbCMnqVPMhxwneVoE9d69ejat2zLFUI/ERm3nKMyq/NRfxzXJgzlDng==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@umbraco-ui/uui-toast-notification": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.4.0.tgz", - "integrity": "sha512-ioiTTxqaOV/2ggnK9/IrnJPf1KRaKEIXd6qrXkMaYH1orCmv3BIdQMnl3TxFOM1YMlnbVZrfxBe2++iqV6TxHA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.5.0.tgz", + "integrity": "sha512-cFjz4/uZudR3yuSqK5gqzAio55ZOOxQAOc8bC5keS0HXL84JcDwrEP4/Nz7X/uUNUqauYZG/iBUirAvqfv7Osw==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-button": "1.4.0", + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-button": "1.5.0", "@umbraco-ui/uui-css": "1.4.0", - "@umbraco-ui/uui-icon": "1.4.0", - "@umbraco-ui/uui-icon-registry-essential": "1.4.0" + "@umbraco-ui/uui-icon": "1.5.0", + "@umbraco-ui/uui-icon-registry-essential": "1.5.0" } }, "node_modules/@umbraco-ui/uui-toast-notification-container": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.4.0.tgz", - "integrity": "sha512-VIftKhOoQ0EdtM9pvDUM2IcvR8S9Fveh/QwMHgGLVlsgUogBNkCPGJKLfh9hzE5RS2v9FdPIkk72qP2A4fpspQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.5.0.tgz", + "integrity": "sha512-AB4kwgocUeDwkxiCYNH0AOMEtExDS6sEq9sk2i8AGDAEjprAB3m0HM9AlrA+T0V1GtSuv+Q1DEuCyxnVbuK0WQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-toast-notification": "1.4.0" + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-toast-notification": "1.5.0" } }, "node_modules/@umbraco-ui/uui-toast-notification-layout": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.4.0.tgz", - "integrity": "sha512-Secrk5+GlZYzOrg1MQ28+rLGW5krXYxYSAhSe5uDKOqTFLjuag7/qiraQDG3xBtf9ZfAAJ3qUy9n50adshoDbA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.5.0.tgz", + "integrity": "sha512-rM7cGCdMolhsndfZT9zGAPI9P3bl1lNpjDhWI124Mgx+KS8t2Q2h9O+7FGqFnjCTJOQES1pdQ+enl2NxCuEkNg==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", + "@umbraco-ui/uui-base": "1.5.0", "@umbraco-ui/uui-css": "1.4.0" } }, - "node_modules/@umbraco-ui/uui-toggle": { + "node_modules/@umbraco-ui/uui-toast-notification-layout/node_modules/@umbraco-ui/uui-css": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.4.0.tgz", - "integrity": "sha512-APIOxs96fcn6HvD/SksN7rhEk6IAta7XU6s0T2Fa+RPIeOBS0NbbvFUX6hW3qjpiD5DdsjOpO2jn/R1fH3nqnQ==", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.4.0.tgz", + "integrity": "sha512-HBCFPuXJijeZbjnjdqmg3oqOGB3RmpQKT/s/Uy0TSJfaQGfz0e73o2eRghYHWF2rdqHw6brKFrZTZHBVvCE/xA==", "dependencies": { - "@umbraco-ui/uui-base": "1.4.0", - "@umbraco-ui/uui-boolean-input": "1.4.0" + "lit": "^2.2.2" + } + }, + "node_modules/@umbraco-ui/uui-toast-notification/node_modules/@umbraco-ui/uui-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.4.0.tgz", + "integrity": "sha512-HBCFPuXJijeZbjnjdqmg3oqOGB3RmpQKT/s/Uy0TSJfaQGfz0e73o2eRghYHWF2rdqHw6brKFrZTZHBVvCE/xA==", + "dependencies": { + "lit": "^2.2.2" + } + }, + "node_modules/@umbraco-ui/uui-toggle": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.5.0.tgz", + "integrity": "sha512-vsJSpBSmlrLzspCa1dGQGYXfc6RwTGTzSlNQdnzzP7qefVRP4GlOaqYV0TJhHMcYdbai+iEkrLznzJQvM9JFLA==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0", + "@umbraco-ui/uui-boolean-input": "1.5.0" + } + }, + "node_modules/@umbraco-ui/uui-visually-hidden": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-visually-hidden/-/uui-visually-hidden-1.5.0.tgz", + "integrity": "sha512-3Imqxp8+hvirakPogqzvRlU+uhshpGRdrEMU7phCS5VGzDEl8NL1BhxR31EQAw7DspwbD5non3ZwbTwLYydfCg==", + "dependencies": { + "@umbraco-ui/uui-base": "1.5.0" } }, "node_modules/@xmldom/xmldom": { @@ -2502,9 +2555,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", - "integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", "dev": true, "dependencies": { "esbuild": "^0.18.10", diff --git a/src/Umbraco.Web.UI.Login/package.json b/src/Umbraco.Web.UI.Login/package.json index 58a033f605..57c7793b75 100644 --- a/src/Umbraco.Web.UI.Login/package.json +++ b/src/Umbraco.Web.UI.Login/package.json @@ -13,15 +13,15 @@ "npm": ">=10.1" }, "dependencies": { - "@umbraco-ui/uui": "^1.4.0", - "@umbraco-ui/uui-css": "^1.4.0", + "@umbraco-ui/uui": "^1.5.0", + "@umbraco-ui/uui-css": "^1.5.0", "lit": "^2.8.0", "msw": "^1.3.2", "rxjs": "^7.8.1" }, "devDependencies": { "typescript": "^5.2.2", - "vite": "^4.4.11" + "vite": "^4.5.0" }, "msw": { "workerDirectory": "public" diff --git a/src/Umbraco.Web.UI.Login/src/auth-styles.css b/src/Umbraco.Web.UI.Login/src/auth-styles.css new file mode 100644 index 0000000000..457c7b2c26 --- /dev/null +++ b/src/Umbraco.Web.UI.Login/src/auth-styles.css @@ -0,0 +1,34 @@ +body { + margin: 0; + padding: 0; +} +#umb-login-form umb-login-input { + width: 100%; + height: 38px; + box-sizing: border-box; + display: block; + border: 1px solid var(--uui-color-border); + border-radius: var(--uui-border-radius); + outline: none; + background-color: var(--uui-color-surface); +} +#umb-login-form umb-login-input input { + width: 100%; + height: 100%; + display: block; + box-sizing: border-box; + border: none; + background: none; + outline: none; + padding: var(--uui-size-1, 3px) var(--uui-size-space-3, 9px); +} +#umb-login-form uui-form-layout-item { + margin-top: var(--uui-size-space-4); + margin-bottom: var(--uui-size-space-4); +} +#umb-login-form umb-login-input:focus-within { + border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1)); +} +#umb-login-form umb-login-input:hover:not(:focus-within) { + border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2)); +} diff --git a/src/Umbraco.Web.UI.Login/src/auth.element.ts b/src/Umbraco.Web.UI.Login/src/auth.element.ts index 96738588c7..dc3490a6a6 100644 --- a/src/Umbraco.Web.UI.Login/src/auth.element.ts +++ b/src/Umbraco.Web.UI.Login/src/auth.element.ts @@ -5,121 +5,247 @@ import { until } from 'lit/directives/until.js'; import { umbAuthContext } from './context/auth.context.js'; import { umbLocalizationContext } from './external/localization/localization-context.js'; +import { UmbLocalizeElement } from './external/localization/localize.element.js'; +import type { UmbLoginInputElement } from './components/login-input.element.js'; +import { InputType, UUIFormLayoutItemElement, UUILabelElement } from '@umbraco-ui/uui'; + +import authStyles from './auth-styles.css?inline'; + +const createInput = (opts: {id: string, type: InputType, name: string, autocomplete: AutoFill, requiredMessage: string, label: string, inputmode: string}) => { + const input = document.createElement('umb-login-input'); + input.type = opts.type; + input.name = opts.name; + input.autocomplete = opts.autocomplete; + input.id = opts.id; + input.required = true; + input.requiredMessage = opts.requiredMessage; + input.label = opts.label; + input.spellcheck = false; + input.inputMode = opts.inputmode; + + return input; +}; + +const createLabel = (opts: {forId: string, localizeAlias: string}) => { + const label = document.createElement('uui-label'); + const umbLocalize = document.createElement('umb-localize') as UmbLocalizeElement; + umbLocalize.key = opts.localizeAlias; + label.for = opts.forId; + label.appendChild(umbLocalize); + + return label; +}; + +const createFormLayoutItem = (label: UUILabelElement, input: UmbLoginInputElement) => { + const formLayoutItem = document.createElement('uui-form-layout-item') as UUIFormLayoutItemElement; + formLayoutItem.appendChild(label); + formLayoutItem.appendChild(input); + + return formLayoutItem; +}; + +const createForm = (elements: HTMLElement[]) => { + const styles = document.createElement('style'); + styles.innerHTML = authStyles; + const form = document.createElement('form'); + form.id = 'umb-login-form'; + form.name = 'login-form'; + + elements.push(styles); + elements.forEach((element) => form.appendChild(element)); + + return form; +}; @customElement('umb-auth') export default class UmbAuthElement extends LitElement { - #returnPath = ''; + #returnPath = ''; - /** - * Disables the local login form and only allows external login providers. - * - * @attr disable-local-login - */ - @property({ type: Boolean, attribute: 'disable-local-login' }) - set disableLocalLogin(value: boolean) { - umbAuthContext.disableLocalLogin = value; - } + /** + * Disables the local login form and only allows external login providers. + * + * @attr disable-local-login + */ + @property({ type: Boolean, attribute: 'disable-local-login' }) + set disableLocalLogin(value: boolean) { + umbAuthContext.disableLocalLogin = value; + } - @property({ type: String, attribute: 'background-image' }) - backgroundImage?: string; + @property({ attribute: 'background-image' }) + backgroundImage?: string; - @property({ type: String, attribute: 'logo-image' }) - logoImage?: string; + @property({ attribute: 'logo-image' }) + logoImage?: string; - @property({ type: Boolean, attribute: 'username-is-email' }) - usernameIsEmail = false; + @property({ type: Boolean, attribute: 'username-is-email' }) + usernameIsEmail = false; - @property({ type: Boolean, attribute: 'allow-password-reset' }) - allowPasswordReset = false; + @property({ type: Boolean, attribute: 'allow-password-reset' }) + allowPasswordReset = false; - @property({ type: Boolean, attribute: 'allow-user-invite' }) - allowUserInvite = false; + @property({ type: Boolean, attribute: 'allow-user-invite' }) + allowUserInvite = false; - @property({ type: String, attribute: 'return-url' }) - set returnPath(value: string) { - this.#returnPath = value; - umbAuthContext.returnPath = this.returnPath; - } - get returnPath() { - // Check if there is a ?redir querystring or else return the returnUrl attribute - return new URLSearchParams(window.location.search).get('returnPath') || this.#returnPath; - } + @property({ type: String, attribute: 'return-url' }) + set returnPath(value: string) { + this.#returnPath = value; + umbAuthContext.returnPath = this.returnPath; + } + get returnPath() { + // Check if there is a ?redir querystring or else return the returnUrl attribute + return new URLSearchParams(window.location.search).get('returnPath') || this.#returnPath; + } - /** - * Override the default flow. - */ - protected flow?: 'mfa' | 'reset-password' | 'invite-user'; + /** + * Override the default flow. + */ + protected flow?: 'mfa' | 'reset-password' | 'invite-user'; - constructor() { - super(); - this.classList.add('uui-text'); - this.classList.add('uui-font'); + _form?: HTMLFormElement; + _usernameLayoutItem?: UUIFormLayoutItemElement; + _passwordLayoutItem?: UUIFormLayoutItemElement; + _usernameInput?: UmbLoginInputElement; + _passwordInput?: UmbLoginInputElement; + _usernameLabel?: UUILabelElement; + _passwordLabel?: UUILabelElement; - (this as unknown as EventTarget).addEventListener('umb-login-flow', (e) => { - if (e instanceof CustomEvent) { - this.flow = e.detail.flow || undefined; - } - this.requestUpdate(); - }); - } + constructor() { + super(); + this.classList.add('uui-text'); + this.classList.add('uui-font'); - render() { - return html` - + (this as unknown as EventTarget).addEventListener('umb-login-flow', (e) => { + if (e instanceof CustomEvent) { + this.flow = e.detail.flow || undefined; + } + this.requestUpdate(); + }); + } + + connectedCallback() { + super.connectedCallback(); + + this.#initializeForm(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._usernameLayoutItem?.remove(); + this._passwordLayoutItem?.remove(); + this._usernameLabel?.remove(); + this._usernameInput?.remove(); + this._passwordLabel?.remove(); + this._passwordInput?.remove(); + } + + /** + * Creates the login form and adds it to the DOM in the default slot. + * This is done to avoid having to deal with the shadow DOM, which is not supported in Google Chrome for autocomplete/autofill. + * + * @see Track this intent-to-ship for Chrome https://groups.google.com/a/chromium.org/g/blink-dev/c/RY9leYMu5hI?pli=1 + * @private + */ + async #initializeForm() { + const labelUsername = + this.usernameIsEmail + ? await umbLocalizationContext.localize('general_username', undefined, 'Username') + : await umbLocalizationContext.localize('general_email', undefined, 'Email'); + const labelPassword = await umbLocalizationContext.localize('general_password', undefined, 'Password'); + const requiredMessage = await umbLocalizationContext.localize('general_required', undefined, 'Required'); + + this._usernameInput = createInput({ + id: 'username-input', + type: 'text', + name: 'username', + autocomplete: 'username', + requiredMessage, + label: labelUsername, + inputmode: this.usernameIsEmail ? 'email' : '' + }); + this._passwordInput = createInput({ + id: 'password-input', + type: 'password', + name: 'password', + autocomplete: 'current-password', + requiredMessage, + label: labelPassword, + inputmode: '' + }); + this._usernameLabel = createLabel({ forId: 'username-input', localizeAlias: this.usernameIsEmail ? 'general_email' : 'user_username' }); + this._passwordLabel = createLabel({ forId: 'password-input', localizeAlias: 'user_password' }); + + this._usernameLayoutItem = createFormLayoutItem(this._usernameLabel, this._usernameInput); + this._passwordLayoutItem = createFormLayoutItem(this._passwordLabel, this._passwordInput); + + this._form = createForm([this._usernameLayoutItem, this._passwordLayoutItem]); + + this.insertAdjacentElement('beforeend', this._form); + } + + render() { + return html` + ${this._renderFlowAndStatus()} `; - } + } - private _renderFlowAndStatus() { - const searchParams = new URLSearchParams(window.location.search); - let flow = this.flow || searchParams.get('flow')?.toLowerCase(); - const status = searchParams.get('status'); + private _renderFlowAndStatus() { + const searchParams = new URLSearchParams(window.location.search); + let flow = this.flow || searchParams.get('flow')?.toLowerCase(); + const status = searchParams.get('status'); - if (status === 'resetCodeExpired') { - return html` - `; - } + if (status === 'resetCodeExpired') { + return html` + `; + } - if (flow === 'invite-user' && status === 'false') { - return html` - `; - } + if (flow === 'invite-user' && status === 'false') { + return html` + `; + } - // validate - if (flow) { - if (flow === 'mfa' && !umbAuthContext.isMfaEnabled) { - flow = undefined; - } - } + // validate + if (flow) { + if (flow === 'mfa' && !umbAuthContext.isMfaEnabled) { + flow = undefined; + } + } - switch (flow) { - case 'mfa': - return html``; - case 'reset': - return html``; - case 'reset-password': - return html``; - case 'invite-user': - return html``; + switch (flow) { + case 'mfa': + return html``; + case 'reset': + return html``; + case 'reset-password': + return html``; + case 'invite-user': + return html``; - default: - return html` - - - `; - } - } + default: + return html` + + + + `; + } + } } declare global { - interface HTMLElementTagNameMap { - 'umb-auth': UmbAuthElement; - } + interface HTMLElementTagNameMap { + 'umb-auth': UmbAuthElement; + } } diff --git a/src/Umbraco.Web.UI.Login/src/components/external-login-provider.element.ts b/src/Umbraco.Web.UI.Login/src/components/external-login-provider.element.ts index cdab26115e..8a464bf948 100644 --- a/src/Umbraco.Web.UI.Login/src/components/external-login-provider.element.ts +++ b/src/Umbraco.Web.UI.Login/src/components/external-login-provider.element.ts @@ -122,7 +122,7 @@ export class UmbExternalLoginProviderElement extends LitElement { ${this.displayName ? html`
    - + Sign in with ${this.displayName}
    diff --git a/src/Umbraco.Web.UI.Login/src/components/login-input.element.ts b/src/Umbraco.Web.UI.Login/src/components/login-input.element.ts new file mode 100644 index 0000000000..4818c389fe --- /dev/null +++ b/src/Umbraco.Web.UI.Login/src/components/login-input.element.ts @@ -0,0 +1,18 @@ +// make new lit element that extends UUIInputElement + +import { UUIInputElement } from '@umbraco-ui/uui'; +import { customElement } from 'lit/decorators.js'; + +@customElement('umb-login-input') +export class UmbLoginInputElement extends UUIInputElement { + protected createRenderRoot() { + return this; + } + static styles = [...UUIInputElement.styles]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-login-input': UmbLoginInputElement; + } +} diff --git a/src/Umbraco.Web.UI.Login/src/components/pages/login.page.element.ts b/src/Umbraco.Web.UI.Login/src/components/pages/login.page.element.ts index 427aefa31c..ca42466c6b 100644 --- a/src/Umbraco.Web.UI.Login/src/components/pages/login.page.element.ts +++ b/src/Umbraco.Web.UI.Login/src/components/pages/login.page.element.ts @@ -1,6 +1,6 @@ import type { UUIButtonState } from '@umbraco-ui/uui'; import { css, CSSResultGroup, html, LitElement, nothing } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property, queryAssignedElements, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { until } from 'lit/directives/until.js'; @@ -9,225 +9,194 @@ import { umbLocalizationContext } from '../../external/localization/localization @customElement('umb-login-page') export default class UmbLoginPageElement extends LitElement { + @property({ type: Boolean, attribute: 'username-is-email' }) + usernameIsEmail = false; - @property({ type: Boolean, attribute: 'username-is-email' }) - usernameIsEmail = false; + @queryAssignedElements({ flatten: true }) + protected slottedElements?: HTMLFormElement[]; - @property({ type: Boolean, attribute: 'allow-password-reset' }) - allowPasswordReset = false; + @property({ type: Boolean, attribute: 'allow-password-reset' }) + allowPasswordReset = false; - @state() - private _loginState: UUIButtonState = undefined; + @state() + private _loginState: UUIButtonState = undefined; - @state() - private _loginError = ''; + @state() + private _loginError = ''; - @state() - private get disableLocalLogin() { - return umbAuthContext.disableLocalLogin; - } + @state() + private get disableLocalLogin() { + return umbAuthContext.disableLocalLogin; + } - #handleSubmit = async (e: SubmitEvent) => { - e.preventDefault(); + #formElement?: HTMLFormElement; - const form = e.target as HTMLFormElement; - if (!form) return; + async #onSlotChanged() { + this.#formElement = this.slottedElements?.[0]; - if (!form.checkValidity()) return; + if (!this.#formElement) return; - const formData = new FormData(form); + this.#formElement.onsubmit = this.#handleSubmit; + } - const username = formData.get('email') as string; - const password = formData.get('password') as string; - const persist = formData.has('persist'); + #handleSubmit = async (e: SubmitEvent) => { + e.preventDefault(); - this._loginState = 'waiting'; + const form = e.target as HTMLFormElement; + if (!form) return; - const response = await umbAuthContext.login({ - username, - password, - persist, - }); + if (!form.checkValidity()) return; - this._loginError = response.error || ''; - this._loginState = response.error ? 'failed' : 'success'; + const formData = new FormData(form); - // Check for 402 status code indicating that MFA is required - if (response.status === 402) { - umbAuthContext.isMfaEnabled = true; - if (response.twoFactorView) { - umbAuthContext.twoFactorView = response.twoFactorView; - } + const username = formData.get('username') as string; + const password = formData.get('password') as string; + const persist = formData.has('persist'); - this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'mfa' }})); - return; - } + this._loginState = 'waiting'; - if (response.error) { - this.dispatchEvent(new CustomEvent('umb-login-failed', { bubbles: true, composed: true, detail: response })); - return; - } + const response = await umbAuthContext.login({ + username, + password, + persist, + }); - const returnPath = umbAuthContext.returnPath; + this._loginError = response.error || ''; + this._loginState = response.error ? 'failed' : 'success'; - if (returnPath) { - location.href = returnPath; - } + // Check for 402 status code indicating that MFA is required + if (response.status === 402) { + umbAuthContext.isMfaEnabled = true; + if (response.twoFactorView) { + umbAuthContext.twoFactorView = response.twoFactorView; + } - this.dispatchEvent(new CustomEvent('umb-login-success', { bubbles: true, composed: true, detail: response.data })); - }; + this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'mfa' } })); + return; + } - get #greetingLocalizationKey() { - return [ - 'login_greeting0', - 'login_greeting1', - 'login_greeting2', - 'login_greeting3', - 'login_greeting4', - 'login_greeting5', - 'login_greeting6', - ][new Date().getDay()]; - } + if (response.error) { + this.dispatchEvent(new CustomEvent('umb-login-failed', { bubbles: true, composed: true, detail: response })); + return; + } - render() { - return html` + const returnPath = umbAuthContext.returnPath; + + if (returnPath) { + location.href = returnPath; + } + + this.dispatchEvent(new CustomEvent('umb-login-success', { bubbles: true, composed: true, detail: response.data })); + }; + + get #greetingLocalizationKey() { + return [ + 'login_greeting0', + 'login_greeting1', + 'login_greeting2', + 'login_greeting3', + 'login_greeting4', + 'login_greeting5', + 'login_greeting6', + ][new Date().getDay()]; + } + + #onSubmitClick = () => { + this.#formElement?.requestSubmit(); + }; + + render() { + return html`

    ${this.disableLocalLogin - ? nothing - : html` - -
    - - - ${this.usernameIsEmail - ? html`Email` - : html`Name`} - - - + ? nothing + : html` + +
    + ${when( + umbAuthContext.supportsPersistLogin, + () => html` + + Remember me + + ` + )} + ${when( + this.allowPasswordReset, + () => + html`` + )} +
    + - - - Password - - - - -
    - ${when( - umbAuthContext.supportsPersistLogin, - () => html` - - Remember me - - ` - )} - ${when( - this.allowPasswordReset, - () => - html`` - )} -
    - - ${this.#renderErrorMessage()} - - -
    -
    + ${this.#renderErrorMessage()} `} `; - } + } - #renderErrorMessage() { - if (!this._loginError || this._loginState !== 'failed') return nothing; + #renderErrorMessage() { + if (!this._loginError || this._loginState !== 'failed') return nothing; - return html`${this._loginError}`; - } + return html`${this._loginError}`; + } - #handleForgottenPassword() { - this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'reset' }})); - } + #handleForgottenPassword() { + this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'reset' } })); + } - static styles: CSSResultGroup = [ - css` + static styles: CSSResultGroup = [ + css` :host { display: flex; flex-direction: column; } #greeting { + color: var(--uui-color-interactive); text-align: center; - font-weight: 600; - font-size: 1.4rem; - margin: 0 0 var(--uui-size-space-6); - } - - form { - display: flex; - flex-direction: column; - gap: var(--uui-size-space-5); - } - - uui-form-layout-item { - margin: 0; - } - - uui-input, - uui-input-password { - width: 100%; - border-radius: var(--uui-border-radius); + font-weight: 400; + font-size: 1.5rem; + margin: 0 0 var(--uui-size-layout-1); + line-height: 1.2; } #umb-login-button { + margin-top: var(--uui-size-space-4); width: 100%; --uui-button-padding-top-factor: 1.5; --uui-button-padding-bottom-factor: 1.5; } #forgot-password { - cursor: pointer; - background: none; - border: 0; - height: 1rem; - color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */ - gap: var(--uui-size-space-1); - align-self: center; - text-decoration: none; - display: inline-flex; - line-height: 1; - font-size: 14px; - font-family: var(--uui-font-family); + cursor: pointer; + background: none; + border: 0; + height: 1rem; + color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */ + gap: var(--uui-size-space-1); + align-self: center; + text-decoration: none; + display: inline-flex; + line-height: 1; + font-size: 14px; + font-family: var(--uui-font-family); } #forgot-password:hover { @@ -244,11 +213,11 @@ export default class UmbLoginPageElement extends LitElement { justify-content: space-between; } `, - ]; + ]; } declare global { - interface HTMLElementTagNameMap { - 'umb-login-page': UmbLoginPageElement; - } + interface HTMLElementTagNameMap { + 'umb-login-page': UmbLoginPageElement; + } } diff --git a/src/Umbraco.Web.UI.Login/src/index.ts b/src/Umbraco.Web.UI.Login/src/index.ts index b972b395c5..610175a415 100644 --- a/src/Umbraco.Web.UI.Login/src/index.ts +++ b/src/Umbraco.Web.UI.Login/src/index.ts @@ -15,3 +15,5 @@ import './components/external-login-provider.element.js'; import './components/layouts/new-password-layout.element.js'; import './components/layouts/confirmation-layout.element.js'; import './components/layouts/error-layout.element.js'; + +import './components/login-input.element.js'; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DynamicRootServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DynamicRootServiceTests.cs new file mode 100644 index 0000000000..e9d277b325 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DynamicRootServiceTests.cs @@ -0,0 +1,719 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.DynamicRoot; +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +/// +/// Tests covering the DynamicRootService +/// +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[SuppressMessage("ReSharper", "NotNullOrRequiredMemberIsNotInitialized")] +public class DynamicRootServiceTests : UmbracoIntegrationTest +{ + public enum DynamicRootOrigin + { + Root, + Parent, + Current, + Site, + ByKey + } + + public enum DynamicRootStepAlias + { + NearestAncestorOrSelf, + NearestDescendantOrSelf, + FarthestDescendantOrSelf, + } + + protected IContentTypeService ContentTypeService => GetRequiredService(); + + protected IFileService FileService => GetRequiredService(); + + protected ContentService ContentService => (ContentService)GetRequiredService(); + + private DynamicRootService DynamicRootService => (GetRequiredService() as DynamicRootService)!; + + private IDomainService DomainService => GetRequiredService(); + + private ContentType ContentTypeYears { get; set; } + + private ContentType ContentTypeYear { get; set; } + + private ContentType ContentTypeAct { get; set; } + + private ContentType ContentTypeActs { get; set; } + + private ContentType ContentTypeStages { get; set; } + + private ContentType ContentTypeStage { get; set; } + + private Content ContentYears { get; set; } + + private Content ContentYear2022 { get; set; } + + private Content ContentActs2022 { get; set; } + + private Content ContentAct2022RanD { get; set; } + + private Content ContentStages2022 { get; set; } + + private Content ContentStage2022Red { get; set; } + + private Content ContentStage2022Blue { get; set; } + + private Content ContentYear2023 { get; set; } + + private Content ContentYear2024 { get; set; } + + private Content Trashed { get; set; } + + + [SetUp] + public new void Setup() + { + // Root + // - Years (years) + // - 2022 (year) + // - Acts + // - Ran-D (Act) + // - Stages (stages) + // - Red (Stage) + // - Blue (Stage) + // - 2023 + // - Acts + // - Stages + // - 2024 + // - Acts + // - Stages + + // NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. + var template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + // DocTypes + ContentTypeAct = ContentTypeBuilder.CreateSimpleContentType("act", "Act", defaultTemplateId: template.Id); + ContentTypeAct.Key = new Guid("B3A50C84-5F6E-473A-A0B5-D41CBEC4EB36"); + ContentTypeService.Save(ContentTypeAct); + + ContentTypeStage = ContentTypeBuilder.CreateSimpleContentType("stage", "Stage", defaultTemplateId: template.Id); + ContentTypeStage.Key = new Guid("C6DCDB3C-9D4B-4F91-9D1C-8C3B74AECA45"); + ContentTypeService.Save(ContentTypeStage); + + ContentTypeStages = + ContentTypeBuilder.CreateSimpleContentType("stages", "Stages", defaultTemplateId: template.Id); + ContentTypeStages.Key = new Guid("BFC4C6C1-51D0-4538-B818-042BEEA0461E"); + ContentTypeStages.AllowedContentTypes = new[] { new ContentTypeSort(ContentTypeStage.Id, 0) }; + ContentTypeService.Save(ContentTypeStages); + + ContentTypeActs = ContentTypeBuilder.CreateSimpleContentType("acts", "Acts", defaultTemplateId: template.Id); + ContentTypeActs.Key = new Guid("110B6BC7-59E0-427D-B350-E488786788E7"); + ContentTypeActs.AllowedContentTypes = new[] { new ContentTypeSort(ContentTypeAct.Id, 0) }; + ContentTypeService.Save(ContentTypeActs); + + ContentTypeYear = ContentTypeBuilder.CreateSimpleContentType("year", "Year", defaultTemplateId: template.Id); + ContentTypeYear.Key = new Guid("001E9029-6BF9-4A68-B11E-7730109E4E28"); + ContentTypeYear.AllowedContentTypes = new[] + { + new ContentTypeSort(ContentTypeStages.Id, 0), new ContentTypeSort(ContentTypeActs.Id, 1), + }; + ContentTypeService.Save(ContentTypeYear); + + ContentTypeYears = ContentTypeBuilder.CreateSimpleContentType("years", "Years", defaultTemplateId: template.Id); + ContentTypeYears.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); + ContentTypeActs.AllowedContentTypes = new[] { new ContentTypeSort(ContentTypeYear.Id, 0) }; + ContentTypeService.Save(ContentTypeYears); + + ContentYears = ContentBuilder.CreateSimpleContent(ContentTypeYears, "Years"); + ContentYears.Key = new Guid("CD3BBE28-D03F-422B-9DC6-A0E591543A8E"); + ContentService.Save(ContentYears, -1); + + ContentYear2022 = ContentBuilder.CreateSimpleContent(ContentTypeYear, "2022", ContentYears.Id); + ContentYear2022.Key = new Guid("9B3066E3-3CE9-4DF6-82C7-444236FF4DAC"); + ContentService.Save(ContentYear2022, -1); + + ContentActs2022 = ContentBuilder.CreateSimpleContent(ContentTypeActs, "Acts", ContentYear2022.Id); + ContentActs2022.Key = new Guid("6FD7F030-269D-45BE-BEB4-030FF8764B6D"); + ContentService.Save(ContentActs2022, -1); + + ContentAct2022RanD = ContentBuilder.CreateSimpleContent(ContentTypeAct, "Ran-D", ContentActs2022.Id); + ContentAct2022RanD.Key = new Guid("9BE4C615-240E-4616-BB65-C1F2DE9C3873"); + ContentService.Save(ContentAct2022RanD, -1); + + ContentStages2022 = ContentBuilder.CreateSimpleContent(ContentTypeStages, "Stages", ContentYear2022.Id); + ContentStages2022.Key = new Guid("1FF59D2F-FCE8-455B-98A6-7686BF41FD33"); + ContentService.Save(ContentStages2022, -1); + + ContentStage2022Red = ContentBuilder.CreateSimpleContent(ContentTypeStage, "Red", ContentStages2022.Id); + ContentStage2022Red.Key = new Guid("F1C4E4D6-FFDE-4053-9240-EC594CE2A073"); + ContentService.Save(ContentStage2022Red, -1); + + ContentStage2022Blue = ContentBuilder.CreateSimpleContent(ContentTypeStage, "Blue", ContentStages2022.Id); + ContentStage2022Blue.Key = new Guid("085311BB-2E75-4FB3-AC30-05F8CF2D3CB5"); + ContentService.Save(ContentStage2022Blue, -1); + + ContentYear2023 = ContentBuilder.CreateSimpleContent(ContentTypeYear, "2023", ContentYears.Id); + ContentYear2023.Key = new Guid("2A863C61-8422-4863-8818-795711FFF0FC"); + ContentService.Save(ContentYear2023, -1); + + ContentYear2024 = ContentBuilder.CreateSimpleContent(ContentTypeYear, "2024", ContentYears.Id); + ContentYear2024.Key = new Guid("E547A970-3923-4EF0-9EDA-10CB83FF038F"); + ContentService.Save(ContentYear2024, -1); + + Trashed = ContentBuilder.CreateSimpleContent(ContentTypeYears, "Text Page Deleted", -20); + Trashed.Trashed = true; + ContentService.Save(Trashed, -1); + } + + + [Test] + public async Task GetDynamicRoots__With_NearestAncestorOrSelf_and_filter_of_own_doc_type_should_return_self() + { + // Arrange + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Current.ToString(), + OriginKey = null, + Context = + new DynamicRootContext() { CurrentKey = ContentAct2022RanD.Key, ParentKey = ContentActs2022.Key }, + QuerySteps = new DynamicRootQueryStep[] + { + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestAncestorOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentAct2022RanD.ContentType.Key }, + }, + }, + }; + + // Act + var result = (await DynamicRootService.GetDynamicRootsAsync(startNodeSelector)).ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Count()); + CollectionAssert.Contains(result, startNodeSelector.Context.CurrentKey.Value); + }); + } + + [Test] + public async Task GetDynamicRoots__With_NearestAncestorOrSelf_and_origin_root_should_return_empty_list() + { + // Arrange + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Root.ToString(), + OriginKey = null, + Context = + new DynamicRootContext() { CurrentKey = ContentAct2022RanD.Key, ParentKey = ContentActs2022.Key }, + QuerySteps = new DynamicRootQueryStep[] + { + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestAncestorOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentAct2022RanD.ContentType.Key }, + }, + }, + }; + + // Act + var result = await DynamicRootService.GetDynamicRootsAsync(startNodeSelector); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(0, result.Count()); + }); + } + + [Test] + [TestCase(DynamicRootStepAlias.NearestDescendantOrSelf)] + [TestCase(DynamicRootStepAlias.FarthestDescendantOrSelf)] + public async Task + GetDynamicRoots__DescendantOrSelf_must_handle_when_there_is_not_found_any_and_level_becomes_impossible_to_get( + DynamicRootStepAlias dynamicRootAlias) + { + // Arrange + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Current.ToString(), + OriginKey = null, + Context = new DynamicRootContext() + { + CurrentKey = ContentAct2022RanD.Key, ParentKey = ContentActs2022.Key, + }, + QuerySteps = new DynamicRootQueryStep[] + { + new DynamicRootQueryStep() + { + Alias = dynamicRootAlias.ToString(), AnyOfDocTypeKeys = new[] { Guid.NewGuid() } + }, + }, + }; + + // Act + var result = await DynamicRootService.GetDynamicRootsAsync(startNodeSelector); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(0, result.Count()); + }); + } + + [Test] + public async Task GetDynamicRoots__NearestDescendantOrSelf__has_to_find_only_the_nearest() + { + // Arrange + + // Allow atc to add acts + ContentTypeAct.AllowedContentTypes = + ContentTypeAct.AllowedContentTypes!.Union(new ContentTypeSort[] + { + new ContentTypeSort(ContentTypeActs.Id, 0), + }); + ContentTypeService.Save(ContentTypeAct); + + var contentNewActs = ContentBuilder.CreateSimpleContent(ContentTypeActs, "new Acts", ContentAct2022RanD.Id); + contentNewActs.Key = new Guid("EA309F8C-8F1A-4C19-9613-2F950CDDCB8D"); + ContentService.Save(contentNewActs, -1); + + var contentNewAct = + ContentBuilder.CreateSimpleContent(ContentTypeAct, "new act under new acts", contentNewActs.Id); + contentNewAct.Key = new Guid("7E14BA13-C998-46DE-92AE-8E1C18CCEE02"); + ContentService.Save(contentNewAct, -1); + + + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Root.ToString(), + OriginKey = null, + Context = new DynamicRootContext() { CurrentKey = contentNewAct.Key, ParentKey = contentNewActs.Key }, + QuerySteps = new[] + { + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestDescendantOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentTypeActs.Key }, + }, + }, + }; + + // Act + var result = (await DynamicRootService.GetDynamicRootsAsync(startNodeSelector)).ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Count()); + CollectionAssert.Contains(result, ContentActs2022.Key); + }); + } + + [Test] + public async Task GetDynamicRoots__FarthestDescendantOrSelf__has_to_find_only_the_farthest() + { + // Arrange + + // Allow act to add acts + ContentTypeAct.AllowedContentTypes = + ContentTypeAct.AllowedContentTypes!.Union(new[] { new ContentTypeSort(ContentTypeActs.Id, 0) }); + ContentTypeService.Save(ContentTypeAct); + + var contentNewActs = ContentBuilder.CreateSimpleContent(ContentTypeActs, "new Acts", ContentAct2022RanD.Id); + contentNewActs.Key = new Guid("EA309F8C-8F1A-4C19-9613-2F950CDDCB8D"); + ContentService.Save(contentNewActs, -1); + + var contentNewAct = + ContentBuilder.CreateSimpleContent(ContentTypeAct, "new act under new acts", contentNewActs.Id); + contentNewAct.Key = new Guid("7E14BA13-C998-46DE-92AE-8E1C18CCEE02"); + ContentService.Save(contentNewAct, -1); + + + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Root.ToString(), + OriginKey = null, + Context = new DynamicRootContext() { CurrentKey = contentNewAct.Key, ParentKey = contentNewActs.Key }, + QuerySteps = new[] + { + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.FarthestDescendantOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentTypeActs.Key }, + }, + }, + }; + + // Act + var result = (await DynamicRootService.GetDynamicRootsAsync(startNodeSelector)).ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Count()); + CollectionAssert.Contains(result, contentNewActs.Key); + }); + } + + [Test] + public async Task GetDynamicRoots__With_multiple_filters() + { + // Arrange + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Current.ToString(), + OriginKey = null, + Context = + new DynamicRootContext() { CurrentKey = ContentAct2022RanD.Key, ParentKey = ContentActs2022.Key }, + QuerySteps = new[] + { + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestAncestorOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentTypeYear.Key }, + }, + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestDescendantOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentTypeStages.Key }, + }, + }, + }; + + // Act + var result = (await DynamicRootService.GetDynamicRootsAsync(startNodeSelector)).ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Count()); + CollectionAssert.Contains(result, ContentStages2022.Key); + }); + } + + [Test] + public async Task GetDynamicRoots__With_NearestDescendantOrSelf_and_filter_of_own_doc_type_should_return_self() + { + // Arrange + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Current.ToString(), + OriginKey = null, + Context = new DynamicRootContext() { CurrentKey = ContentYear2022.Key, ParentKey = ContentYears.Key }, + QuerySteps = new DynamicRootQueryStep[] + { + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestDescendantOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentYear2022.ContentType.Key }, + }, + }, + }; + + // Act + var result = (await DynamicRootService.GetDynamicRootsAsync(startNodeSelector)).ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Count()); + CollectionAssert.Contains(result, startNodeSelector.Context.CurrentKey.Value); + }); + } + + + [Test] + public async Task GetDynamicRoots__With_no_filters_should_return_what_origin_finds() + { + // Arrange + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Parent.ToString(), + OriginKey = null, + Context = new DynamicRootContext() { CurrentKey = ContentYear2022.Key, ParentKey = ContentYears.Key }, + QuerySteps = Array.Empty(), + }; + + // Act + var result = (await DynamicRootService.GetDynamicRootsAsync(startNodeSelector)).ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Count()); + CollectionAssert.Contains(result, startNodeSelector.Context.ParentKey); + }); + } + + + [Test] + public void CalculateOriginKey__Parent_should_just_return_the_parent_key() + { + // Arrange + var selector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Parent.ToString(), + OriginKey = null, + Context = new DynamicRootContext() { CurrentKey = ContentYear2022.Key, ParentKey = ContentYears.Key }, + }; + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.AreEqual(selector.Context.ParentKey, result); + } + + [Test] + public void CalculateOriginKey__Current_should_just_return_the_current_key_when_it_exists() + { + // Arrange + var selector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Current.ToString(), + OriginKey = null, + Context = new DynamicRootContext() { CurrentKey = ContentYear2022.Key, ParentKey = ContentYears.Key }, + }; + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.AreEqual(selector.Context.CurrentKey, result); + } + + [Test] + public void CalculateOriginKey__Current_should_just_return_null_when_it_does_not_exist() + { + // Arrange + var selector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Current.ToString(), + OriginKey = null, + Context = new DynamicRootContext() { CurrentKey = Guid.NewGuid(), ParentKey = ContentYears.Key }, + }; + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.IsNull(result); + } + + [Test] + public void CalculateOriginKey__Root_should_traverse_the_path_and_take_the_first_level_in_the_root() + { + // Arrange + var selector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Root.ToString(), + OriginKey = null, + Context = new DynamicRootContext() + { + CurrentKey = ContentAct2022RanD.Key, ParentKey = ContentActs2022.Key, + }, + }; + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.AreEqual(ContentYears.Key, result); + } + + [Test] + public void CalculateOriginKey__Site_should_return_the_first_with_an_assigned_domain_also_it_self() + { + // Arrange + var origin = ContentYear2022; + var selector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Site.ToString(), + OriginKey = origin.Key, + Context = new DynamicRootContext() { CurrentKey = origin.Key, ParentKey = ContentYears.Key }, + }; + + DomainService.Save( + new UmbracoDomain("http://test.umbraco.com") { RootContentId = origin.Id, LanguageIsoCode = "en-us" }); + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.AreEqual(origin.Key, result); + } + + [Test] + public void CalculateOriginKey__Site_should_return_the_first_with_an_assigned_domain() + { + // Arrange + var origin = ContentAct2022RanD; + var selector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Site.ToString(), + OriginKey = origin.Key, + Context = new DynamicRootContext() { CurrentKey = origin.Key, ParentKey = ContentActs2022.Key }, + }; + + DomainService.Save(new UmbracoDomain("http://test.umbraco.com") + { + RootContentId = ContentYears.Id, + LanguageIsoCode = "en-us", + }); + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.AreEqual(ContentYears.Key, result); + } + + [Test] + public void CalculateOriginKey__Site_should_fallback_to_root_when_no_domain_is_assigned() + { + // Arrange + var selector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Site.ToString(), + OriginKey = ContentActs2022.Key, + Context = new DynamicRootContext() + { + CurrentKey = ContentAct2022RanD.Key, + ParentKey = ContentActs2022.Key, + }, + }; + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.AreEqual(ContentYears.Key, result); + } + + [Test] + [TestCase(DynamicRootOrigin.ByKey)] + [TestCase(DynamicRootOrigin.Parent)] + [TestCase(DynamicRootOrigin.Root)] + [TestCase(DynamicRootOrigin.Site)] + [TestCase(DynamicRootOrigin.Site)] + public void CalculateOriginKey__with_a_random_key_should_return_null(DynamicRootOrigin origin) + { + // Arrange + var randomKey = Guid.NewGuid(); + var selector = new DynamicRootNodeQuery() + { + OriginAlias = origin.ToString(), + OriginKey = randomKey, + Context = new DynamicRootContext() { CurrentKey = randomKey, ParentKey = Guid.NewGuid() }, + }; + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.IsNull(result); + } + + [Test] + [TestCase(DynamicRootOrigin.ByKey)] + [TestCase(DynamicRootOrigin.Parent)] + [TestCase(DynamicRootOrigin.Root)] + [TestCase(DynamicRootOrigin.Site)] + [TestCase(DynamicRootOrigin.Current)] + public void CalculateOriginKey__with_a_trashed_key_should_still_be_allowed(DynamicRootOrigin origin) + { + // Arrange + var trashedKey = Trashed.Key; + var selector = new DynamicRootNodeQuery() + { + OriginAlias = origin.ToString(), + OriginKey = trashedKey, + Context = new DynamicRootContext() { CurrentKey = trashedKey, ParentKey = trashedKey }, + }; + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.IsNotNull(result); + } + + [Test] + [TestCase(DynamicRootOrigin.ByKey)] + [TestCase(DynamicRootOrigin.Parent)] + [TestCase(DynamicRootOrigin.Root)] + [TestCase(DynamicRootOrigin.Site)] + [TestCase(DynamicRootOrigin.Current)] + public void CalculateOriginKey__with_a_ContentType_key_should_return_null(DynamicRootOrigin origin) + { + // Arrange + var contentTypeKey = ContentTypeYears.Key; + var selector = new DynamicRootNodeQuery() + { + OriginAlias = origin.ToString(), + OriginKey = contentTypeKey, + Context = new DynamicRootContext() { CurrentKey = contentTypeKey, ParentKey = contentTypeKey } + }; + + // Act + var result = DynamicRootService.FindOriginKey(selector); + + // Assert + Assert.IsNull(result); + } + + [Test] + public async Task GetDynamicRoots__With_multiple_filters_that_do_not_return_any_results() + { + // Arrange + var startNodeSelector = new DynamicRootNodeQuery() + { + OriginAlias = DynamicRootOrigin.Current.ToString(), + OriginKey = null, + Context = + new DynamicRootContext() { CurrentKey = ContentAct2022RanD.Key, ParentKey = ContentActs2022.Key }, + QuerySteps = new[] + { + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestAncestorOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentTypeYear.Key }, + }, + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestDescendantOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentTypeStages.Key }, + }, + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestDescendantOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentTypeYears.Key }, + }, + new DynamicRootQueryStep() + { + Alias = DynamicRootStepAlias.NearestDescendantOrSelf.ToString(), + AnyOfDocTypeKeys = new[] { ContentTypeYears.Key }, + }, + }, + }; + + // Act + var result = (await DynamicRootService.GetDynamicRootsAsync(startNodeSelector)).ToList(); + + // Assert + Assert.AreEqual(0, result.Count()); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs new file mode 100644 index 0000000000..af75becd1c --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs @@ -0,0 +1,50 @@ +using System.Globalization; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Webhooks; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class WebhookLogServiceTests : UmbracoIntegrationTest +{ + private IWebhookLogService WebhookLogService => GetRequiredService(); + + [Test] + public async Task Can_Create_And_Get() + { + var createdWebhookLog = await WebhookLogService.CreateAsync(new WebhookLog + { + Date = DateTime.UtcNow, + EventName = Constants.WebhookEvents.ContentPublish, + RequestBody = "Test Request Body", + ResponseBody = "Test response body", + StatusCode = "200", + RetryCount = 0, + Key = Guid.NewGuid(), + }); + + + var webhookLogsPaged = await WebhookLogService.Get(); + + Assert.Multiple(() => + { + Assert.IsNotNull(webhookLogsPaged); + Assert.IsNotEmpty(webhookLogsPaged.Items); + Assert.AreEqual(1, webhookLogsPaged.Items.Count()); + var webHookLog = webhookLogsPaged.Items.First(); + Assert.AreEqual(createdWebhookLog.Date.ToString(CultureInfo.InvariantCulture), webHookLog.Date.ToString(CultureInfo.InvariantCulture)); + Assert.AreEqual(createdWebhookLog.EventName, webHookLog.EventName); + Assert.AreEqual(createdWebhookLog.RequestBody, webHookLog.RequestBody); + Assert.AreEqual(createdWebhookLog.ResponseBody, webHookLog.ResponseBody); + Assert.AreEqual(createdWebhookLog.StatusCode, webHookLog.StatusCode); + Assert.AreEqual(createdWebhookLog.RetryCount, webHookLog.RetryCount); + Assert.AreEqual(createdWebhookLog.Key, webHookLog.Key); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs new file mode 100644 index 0000000000..6f6da74485 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -0,0 +1,106 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class WebhookServiceTests : UmbracoIntegrationTest +{ + private IWebHookService WebhookService => GetRequiredService(); + + [Test] + [TestCase("https://example.com", Constants.WebhookEvents.ContentPublish, "00000000-0000-0000-0000-010000000000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentDelete, "00000000-0000-0000-0000-000200000000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] + [TestCase("https://example.com", Constants.WebhookEvents.MediaDelete, "00000000-0000-0000-0000-000004000000")] + [TestCase("https://example.com", Constants.WebhookEvents.MediaSave, "00000000-0000-0000-0000-000000500000")] + public async Task Can_Create_And_Get(string url, string webhookEvent, Guid key) + { + var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, true, new[] { key }, new[] { webhookEvent })); + var webhook = await WebhookService.GetAsync(createdWebhook.Key); + + Assert.Multiple(() => + { + Assert.IsNotNull(webhook); + Assert.AreEqual(1, webhook.Events.Length); + Assert.IsTrue(webhook.Events.Contains(webhookEvent)); + Assert.AreEqual(url, webhook.Url); + Assert.IsTrue(webhook.ContentTypeKeys.Contains(key)); + }); + } + + [Test] + public async Task Can_Get_All() + { + var createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentPublish })); + var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentDelete })); + var createdWebhookThree = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.ContentUnpublish })); + var webhooks = await WebhookService.GetAllAsync(0, int.MaxValue); + + Assert.Multiple(() => + { + Assert.IsNotEmpty(webhooks.Items); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookOne.Key)); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookTwo.Key)); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookThree.Key)); + }); + } + + [Test] + [TestCase("https://example.com", Constants.WebhookEvents.ContentPublish, "00000000-0000-0000-0000-010000000000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentDelete, "00000000-0000-0000-0000-000200000000")] + [TestCase("https://example.com", Constants.WebhookEvents.ContentUnpublish, "00000000-0000-0000-0000-300000000000")] + [TestCase("https://example.com", Constants.WebhookEvents.MediaDelete, "00000000-0000-0000-0000-000004000000")] + [TestCase("https://example.com", Constants.WebhookEvents.MediaSave, "00000000-0000-0000-0000-000000500000")] + public async Task Can_Delete(string url, string webhookEvent, Guid key) + { + var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, true, new[] { key }, new[] { webhookEvent })); + var webhook = await WebhookService.GetAsync(createdWebhook.Key); + + Assert.IsNotNull(webhook); + await WebhookService.DeleteAsync(webhook.Key); + var deletedWebhook = await WebhookService.GetAsync(createdWebhook.Key); + Assert.IsNull(deletedWebhook); + } + + [Test] + public async Task Can_Create_With_No_EntityKeys() + { + var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentPublish })); + var webhook = await WebhookService.GetAsync(createdWebhook.Key); + + Assert.IsNotNull(webhook); + Assert.IsEmpty(webhook.ContentTypeKeys); + } + + [Test] + public async Task Can_Update() + { + var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentPublish })); + createdWebhook.Events = new[] { Constants.WebhookEvents.ContentDelete }; + await WebhookService.UpdateAsync(createdWebhook); + + var updatedWebhook = await WebhookService.GetAsync(createdWebhook.Key); + Assert.IsNotNull(updatedWebhook); + Assert.AreEqual(1, updatedWebhook.Events.Length); + Assert.IsTrue(updatedWebhook.Events.Contains(Constants.WebhookEvents.ContentDelete)); + } + + [Test] + public async Task Can_Get_By_EventName() + { + var webhook1 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentPublish })); + var webhook2 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentUnpublish })); + var webhook3 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentUnpublish })); + + var result = await WebhookService.GetByEventNameAsync(Constants.WebhookEvents.ContentUnpublish); + + Assert.IsNotEmpty(result); + Assert.AreEqual(2, result.Count()); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs new file mode 100644 index 0000000000..faf7a2b566 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs @@ -0,0 +1,179 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class RichTextPropertyEditorTests : UmbracoIntegrationTest +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + [Test] + public void Can_Use_Markup_String_As_Value() + { + var contentType = ContentTypeBuilder.CreateTextPageContentType("myContentType"); + contentType.AllowedTemplates = Enumerable.Empty(); + ContentTypeService.Save(contentType); + + var dataType = DataTypeService.GetDataType(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "bodyText").DataTypeId)!; + var editor = dataType.Editor!; + var valueEditor = editor.GetValueEditor(); + + const string markup = "

    This is some markup

    "; + + var content = ContentBuilder.CreateTextpageContent(contentType, "My Content", -1); + content.Properties["bodyText"]!.SetValue(markup); + ContentService.Save(content); + + var toEditor = valueEditor.ToEditor(content.Properties["bodyText"]); + var richTextEditorValue = toEditor as RichTextEditorValue; + + Assert.IsNotNull(richTextEditorValue); + Assert.AreEqual(markup, richTextEditorValue.Markup); + } + + [Test] + public void Can_Use_RichTextEditorValue_As_Value() + { + var contentType = ContentTypeBuilder.CreateTextPageContentType("myContentType"); + contentType.AllowedTemplates = Enumerable.Empty(); + ContentTypeService.Save(contentType); + + var dataType = DataTypeService.GetDataType(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "bodyText").DataTypeId)!; + var editor = dataType.Editor!; + var valueEditor = editor.GetValueEditor(); + + const string markup = "

    This is some markup

    "; + var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue(new RichTextEditorValue { Markup = markup, Blocks = null }, JsonSerializer); + + var content = ContentBuilder.CreateTextpageContent(contentType, "My Content", -1); + content.Properties["bodyText"]!.SetValue(propertyValue); + ContentService.Save(content); + + var toEditor = valueEditor.ToEditor(content.Properties["bodyText"]); + var richTextEditorValue = toEditor as RichTextEditorValue; + + Assert.IsNotNull(richTextEditorValue); + Assert.AreEqual(markup, richTextEditorValue.Markup); + } + + [Test] + public void Can_Track_Block_References() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var contentType = ContentTypeBuilder.CreateTextPageContentType("myContentType"); + contentType.AllowedTemplates = Enumerable.Empty(); + ContentTypeService.Save(contentType); + + var pickedContent = ContentBuilder.CreateTextpageContent(contentType, "My Content", -1); + ContentService.Save(pickedContent); + + var dataType = DataTypeService.GetDataType(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "bodyText").DataTypeId)!; + var editor = dataType.Editor!; + var valueEditor = (BlockValuePropertyValueEditorBase)editor.GetValueEditor(); + + var elementId = Guid.NewGuid(); + var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( + new RichTextEditorValue + { + Markup = @$"

    This is some markup

    ", + Blocks = JsonSerializer.Deserialize($$""" + { + "layout": { + "Umbraco.TinyMCE": [{ + "contentUdi": "umb://element/{{elementId:N}}" + } + ] + }, + "contentData": [{ + "contentTypeKey": "{{elementType.Key:B}}", + "udi": "umb://element/{{elementId:N}}", + "contentPicker": "umb://document/{{pickedContent.Key:N}}" + } + ], + "settingsData": [] + } + """) + }, + JsonSerializer); + + var content = ContentBuilder.CreateTextpageContent(contentType, "My Content", -1); + content.Properties["bodyText"]!.SetValue(propertyValue); + ContentService.Save(content); + + var references = valueEditor.GetReferences(content.GetValue("bodyText")).ToArray(); + Assert.AreEqual(1, references.Length); + var reference = references.First(); + Assert.AreEqual(Constants.Conventions.RelationTypes.RelatedDocumentAlias, reference.RelationTypeAlias); + Assert.AreEqual(pickedContent.GetUdi(), reference.Udi); + } + + [Test] + public void Can_Track_Block_Tags() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var contentType = ContentTypeBuilder.CreateTextPageContentType("myContentType"); + contentType.AllowedTemplates = Enumerable.Empty(); + ContentTypeService.Save(contentType); + + var dataType = DataTypeService.GetDataType(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "bodyText").DataTypeId)!; + var editor = dataType.Editor!; + var valueEditor = (BlockValuePropertyValueEditorBase)editor.GetValueEditor(); + + var elementId = Guid.NewGuid(); + var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( + new RichTextEditorValue + { + Markup = @$"

    This is some markup

    ", + Blocks = JsonSerializer.Deserialize($$""" + { + "layout": { + "Umbraco.TinyMCE": [{ + "contentUdi": "umb://element/{{elementId:N}}" + } + ] + }, + "contentData": [{ + "contentTypeKey": "{{elementType.Key:B}}", + "udi": "umb://element/{{elementId:N}}", + "tags": "['Tag One', 'Tag Two', 'Tag Three']" + } + ], + "settingsData": [] + } + """) + }, + JsonSerializer); + + var content = ContentBuilder.CreateTextpageContent(contentType, "My Content", -1); + content.Properties["bodyText"]!.SetValue(propertyValue); + ContentService.Save(content); + + var tags = valueEditor.GetTags(content.GetValue("bodyText"), null, null).ToArray(); + Assert.AreEqual(3, tags.Length); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One")); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two")); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three")); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeFinderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeFinderTests.cs index 3089d89893..047e28dda5 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeFinderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeFinderTests.cs @@ -55,10 +55,10 @@ public class TypeFinderTests Assert.AreEqual(0, typesFound.Count()); // 0 classes in _assemblies are marked with [Tree] typesFound = typeFinder.FindClassesWithAttribute(new[] { typeof(TreeAttribute).Assembly }); - Assert.AreEqual(23, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] + Assert.AreEqual(24, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] typesFound = typeFinder.FindClassesWithAttribute(); - Assert.AreEqual(23, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] + Assert.AreEqual(24, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] } [AttributeUsage(AttributeTargets.Class)] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index 2fef067c4c..b7712b5346 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -3,8 +3,11 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Infrastructure.DeliveryApi; @@ -12,7 +15,7 @@ using Umbraco.Cms.Infrastructure.DeliveryApi; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; [TestFixture] -public class RichTextParserTests +public class RichTextParserTests : PropertyValueConverterTests { private readonly Guid _contentKey = Guid.NewGuid(); private readonly Guid _contentRootKey = Guid.NewGuid(); @@ -33,7 +36,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse("

    Some text paragraph

    ") as RichTextGenericElement; + var element = parser.Parse("

    Some text paragraph

    ") as RichTextRootElement; Assert.IsNotNull(element); Assert.AreEqual(1, element.Elements.Count()); var paragraph = element.Elements.Single() as RichTextGenericElement; @@ -49,7 +52,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse("

    Some text
    More text
    Even more text

    ") as RichTextGenericElement; + var element = parser.Parse("

    Some text
    More text
    Even more text

    ") as RichTextRootElement; Assert.IsNotNull(element); Assert.AreEqual(1, element.Elements.Count()); var paragraph = element.Elements.Single() as RichTextGenericElement; @@ -97,7 +100,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse("

    Text in a data-something SPAN

    ") as RichTextGenericElement; + var element = parser.Parse("

    Text in a data-something SPAN

    ") as RichTextRootElement; Assert.IsNotNull(element); var span = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(span); @@ -115,7 +118,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse("

    Text in a data-something SPAN

    ") as RichTextGenericElement; + var element = parser.Parse("

    Text in a data-something SPAN

    ") as RichTextRootElement; Assert.IsNotNull(element); var span = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(span); @@ -130,7 +133,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

    ") as RichTextGenericElement; + var element = parser.Parse($"

    ") as RichTextRootElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -149,7 +152,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

    ") as RichTextGenericElement; + var element = parser.Parse($"

    ") as RichTextRootElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -164,7 +167,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

    ") as RichTextGenericElement; + var element = parser.Parse($"

    ") as RichTextRootElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -179,7 +182,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

    This is the link text

    ") as RichTextGenericElement; + var element = parser.Parse($"

    This is the link text

    ") as RichTextRootElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -195,7 +198,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

    ") as RichTextGenericElement; + var element = parser.Parse($"

    ") as RichTextRootElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -208,7 +211,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

    ") as RichTextGenericElement; + var element = parser.Parse($"

    ") as RichTextRootElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -223,7 +226,7 @@ public class RichTextParserTests { var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

    ") as RichTextGenericElement; + var element = parser.Parse($"

    ") as RichTextRootElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -233,6 +236,128 @@ public class RichTextParserTests Assert.AreEqual("https://some.where/something.png?rmode=max&width=500", link.Attributes.First().Value); } + [Test] + public void ParseElement_RemovesComments() + { + var parser = CreateRichTextElementParser(); + + var element = parser.Parse("

    some textsome more text

    ") as RichTextRootElement; + Assert.IsNotNull(element); + var paragraph = element.Elements.Single() as RichTextGenericElement; + Assert.IsNotNull(paragraph); + Assert.AreEqual(2, paragraph.Elements.Count()); + var textElements = paragraph.Elements.OfType().ToArray(); + Assert.AreEqual(2, textElements.Length); + Assert.AreEqual("some text", textElements.First().Text); + Assert.AreEqual("some more text", textElements.Last().Text); + } + + [TestCase(true)] + [TestCase(false)] + public void ParseElement_CleansUpBlocks(bool inlineBlock) + { + var parser = CreateRichTextElementParser(); + var id = Guid.NewGuid(); + + var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; + var element = parser.Parse($"

    <{tagName} data-content-udi=\"umb://element/{id:N}\">

    ") as RichTextRootElement; + Assert.IsNotNull(element); + var paragraph = element.Elements.Single() as RichTextGenericElement; + Assert.IsNotNull(paragraph); + var block = paragraph.Elements.Single() as RichTextGenericElement; + Assert.IsNotNull(block); + Assert.AreEqual(tagName, block.Tag); + Assert.AreEqual(1, block.Attributes.Count); + Assert.IsTrue(block.Attributes.ContainsKey("content-id")); + Assert.AreEqual(id, block.Attributes["content-id"]); + Assert.IsEmpty(block.Elements); + } + + [TestCase(true)] + [TestCase(false)] + public void ParseElement_AppendsBlocks(bool inlineBlock) + { + var parser = CreateRichTextElementParser(); + var block1ContentId = Guid.NewGuid(); + var block2ContentId = Guid.NewGuid(); + var block2SettingsId = Guid.NewGuid(); + RichTextBlockModel richTextBlockModel = new RichTextBlockModel( + new List + { + new ( + Udi.Create(Constants.UdiEntityType.Element, block1ContentId), + CreateElement(block1ContentId, 123), + null!, + null!), + new ( + Udi.Create(Constants.UdiEntityType.Element, block2ContentId), + CreateElement(block2ContentId, 456), + Udi.Create(Constants.UdiEntityType.Element, block2SettingsId), + CreateElement(block2SettingsId, 789)) + }); + + var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; + var element = parser.Parse($"

    <{tagName} data-content-udi=\"umb://element/{block1ContentId:N}\"><{tagName} data-content-udi=\"umb://element/{block2ContentId:N}\">

    ", richTextBlockModel) as RichTextRootElement; + Assert.IsNotNull(element); + var paragraph = element.Elements.Single() as RichTextGenericElement; + Assert.IsNotNull(paragraph); + Assert.AreEqual(2, paragraph.Elements.Count()); + + var block1Element = paragraph.Elements.First() as RichTextGenericElement; + Assert.IsNotNull(block1Element); + Assert.AreEqual(tagName, block1Element.Tag); + Assert.AreEqual(block1ContentId, block1Element.Attributes["content-id"]); + + var block2Element = paragraph.Elements.Last() as RichTextGenericElement; + Assert.IsNotNull(block2Element); + Assert.AreEqual(tagName, block2Element.Tag); + Assert.AreEqual(block2ContentId, block2Element.Attributes["content-id"]); + + Assert.AreEqual(2, element.Blocks.Count()); + + var block1 = element.Blocks.First(); + Assert.AreEqual(block1ContentId, block1.Content.Id); + Assert.AreEqual(123, block1.Content.Properties["number"]); + Assert.IsNull(block1.Settings); + + var block2 = element.Blocks.Last(); + Assert.AreEqual(block2ContentId, block2.Content.Id); + Assert.AreEqual(456, block2.Content.Properties["number"]); + Assert.AreEqual(block2SettingsId, block2.Settings!.Id); + Assert.AreEqual(789, block2.Settings.Properties["number"]); + } + + [Test] + public void ParseElement_CanHandleMixedInlineAndBlockLevelBlocks() + { + var parser = CreateRichTextElementParser(); + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var element = parser.Parse($"

    ") as RichTextRootElement; + Assert.IsNotNull(element); + Assert.AreEqual(2, element.Elements.Count()); + + var paragraph = element.Elements.First() as RichTextGenericElement; + Assert.IsNotNull(paragraph); + + var inlineBlock = paragraph.Elements.Single() as RichTextGenericElement; + Assert.IsNotNull(inlineBlock); + Assert.AreEqual("umb-rte-block-inline", inlineBlock.Tag); + Assert.AreEqual(1, inlineBlock.Attributes.Count); + Assert.IsTrue(inlineBlock.Attributes.ContainsKey("content-id")); + Assert.AreEqual(id1, inlineBlock.Attributes["content-id"]); + Assert.IsEmpty(inlineBlock.Elements); + + var blockLevelBlock = element.Elements.Last() as RichTextGenericElement; + Assert.IsNotNull(blockLevelBlock); + Assert.AreEqual("umb-rte-block", blockLevelBlock.Tag); + Assert.AreEqual(1, blockLevelBlock.Attributes.Count); + Assert.IsTrue(blockLevelBlock.Attributes.ContainsKey("content-id")); + Assert.AreEqual(id2, blockLevelBlock.Attributes["content-id"]); + Assert.IsEmpty(blockLevelBlock.Elements); + } + [Test] public void ParseMarkup_CanParseContentLink() { @@ -303,6 +428,29 @@ public class RichTextParserTests Assert.AreEqual(html, result); } + [TestCase(true)] + [TestCase(false)] + public void ParseMarkup_CleansUpBlocks(bool inlineBlock) + { + var parser = CreateRichTextMarkupParser(); + var id = Guid.NewGuid(); + + var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; + var result = parser.Parse($"

    <{tagName} data-content-udi=\"umb://element/{id:N}\">

    "); + Assert.AreEqual($"

    <{tagName} data-content-id=\"{id:D}\">

    ", result); + } + + [Test] + public void ParseMarkup_CanHandleMixedInlineAndBlockLevelBlocks() + { + var parser = CreateRichTextMarkupParser(); + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var result = parser.Parse($"

    "); + Assert.AreEqual($"

    ", result); + } + private ApiRichTextElementParser CreateRichTextElementParser() { SetupTestContent(out var routeBuilder, out var snapshotAccessor, out var urlProvider); @@ -311,6 +459,7 @@ public class RichTextParserTests routeBuilder, urlProvider, snapshotAccessor, + new ApiElementBuilder(CreateOutputExpansionStrategyAccessor()), Mock.Of>()); } @@ -362,4 +511,21 @@ public class RichTextParserTests snapshotAccessor = snapshotAccessorMock.Object; urlProvider = urlProviderMock.Object; } + + private IPublishedElement CreateElement(Guid id, int propertyValue) + { + var elementType = new Mock(); + elementType.SetupGet(c => c.Alias).Returns("theElementType"); + elementType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Element); + + var element = new Mock(); + element.SetupGet(c => c.Key).Returns(id); + element.SetupGet(c => c.ContentType).Returns(elementType.Object); + + var numberPropertyType = SetupPublishedPropertyType(new IntegerValueConverter(), "number", Constants.PropertyEditors.Aliases.Label); + var property = new PublishedElementPropertyBase(numberPropertyType, element.Object, false, PropertyCacheLevel.None, propertyValue); + + element.SetupGet(c => c.Properties).Returns(new[] { property }); + return element.Object; + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs new file mode 100644 index 0000000000..89379322ee --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs @@ -0,0 +1,48 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterTestsBase +{ + protected override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.BlockGrid; + + [Test] + public void Get_Value_Type() + { + var editor = CreateConverter(); + var config = ConfigForSingle(); + var propertyType = GetPropertyType(config); + + var valueType = editor.GetPropertyValueType(propertyType); + + // the result is always block grid model + Assert.AreEqual(typeof(BlockGridModel), valueType); + } + + private BlockGridPropertyValueConverter CreateConverter() + { + var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); + var publishedModelFactory = new NoopPublishedModelFactory(); + var editor = new BlockGridPropertyValueConverter( + Mock.Of(), + new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory), + new JsonNetSerializer(), + new ApiElementBuilder(Mock.Of())); + return editor; + } + + private BlockGridConfiguration ConfigForSingle() => new() + { + Blocks = new[] { new BlockGridConfiguration.BlockGridBlockConfiguration { ContentElementTypeKey = ContentKey1 } }, + }; +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs index 7a02ee585d..7bbc031e82 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -10,55 +10,15 @@ using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Serialization; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] -public class BlockListPropertyValueConverterTests +public class BlockListPropertyValueConverterTests : BlockPropertyValueConverterTestsBase { - private readonly Guid _contentKey1 = Guid.NewGuid(); - private readonly Guid _contentKey2 = Guid.NewGuid(); - private const string ContentAlias1 = "Test1"; - private const string ContentAlias2 = "Test2"; - private readonly Guid _settingKey1 = Guid.NewGuid(); - private readonly Guid _settingKey2 = Guid.NewGuid(); - private const string SettingAlias1 = "Setting1"; - private const string SettingAlias2 = "Setting2"; - - /// - /// Setup mocks for IPublishedSnapshotAccessor - /// - private IPublishedSnapshotAccessor GetPublishedSnapshotAccessor() - { - var test1ContentType = Mock.Of(x => - x.IsElement == true - && x.Key == _contentKey1 - && x.Alias == ContentAlias1); - var test2ContentType = Mock.Of(x => - x.IsElement == true - && x.Key == _contentKey2 - && x.Alias == ContentAlias2); - var test3ContentType = Mock.Of(x => - x.IsElement == true - && x.Key == _settingKey1 - && x.Alias == SettingAlias1); - var test4ContentType = Mock.Of(x => - x.IsElement == true - && x.Key == _settingKey2 - && x.Alias == SettingAlias2); - var contentCache = new Mock(); - contentCache.Setup(x => x.GetContentType(_contentKey1)).Returns(test1ContentType); - contentCache.Setup(x => x.GetContentType(_contentKey2)).Returns(test2ContentType); - contentCache.Setup(x => x.GetContentType(_settingKey1)).Returns(test3ContentType); - contentCache.Setup(x => x.GetContentType(_settingKey2)).Returns(test4ContentType); - var publishedSnapshot = Mock.Of(x => x.Content == contentCache.Object); - var publishedSnapshotAccessor = - Mock.Of(x => x.TryGetPublishedSnapshot(out publishedSnapshot)); - return publishedSnapshotAccessor; - } + protected override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.BlockList; private BlockListPropertyValueConverter CreateConverter() { @@ -79,40 +39,31 @@ public class BlockListPropertyValueConverterTests { new BlockListConfiguration.BlockConfiguration { - ContentElementTypeKey = _contentKey1, - SettingsElementTypeKey = _settingKey2, + ContentElementTypeKey = ContentKey1, + SettingsElementTypeKey = SettingKey2, }, new BlockListConfiguration.BlockConfiguration { - ContentElementTypeKey = _contentKey2, - SettingsElementTypeKey = _settingKey1, + ContentElementTypeKey = ContentKey2, + SettingsElementTypeKey = SettingKey1, }, }, }; private BlockListConfiguration ConfigForSingle() => new() { - Blocks = new[] { new BlockListConfiguration.BlockConfiguration { ContentElementTypeKey = _contentKey1 } }, + Blocks = new[] { new BlockListConfiguration.BlockConfiguration { ContentElementTypeKey = ContentKey1 } }, }; private BlockListConfiguration ConfigForSingleBlockMode() => new() { - Blocks = new[] { new BlockListConfiguration.BlockConfiguration { ContentElementTypeKey = _contentKey1 } }, + Blocks = new[] { new BlockListConfiguration.BlockConfiguration { ContentElementTypeKey = ContentKey1 } }, ValidationLimit = new() { Min = 1, Max = 1 }, UseSingleBlockMode = true, }; - private IPublishedPropertyType GetPropertyType(BlockListConfiguration config) - { - var dataType = new PublishedDataType(1, "test", new Lazy(() => config)); - var propertyType = Mock.Of(x => - x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList - && x.DataType == dataType); - return propertyType; - } - [Test] - public void Is_Converter_For() + public void IsConverter_For() { var editor = CreateConverter(); Assert.IsTrue(editor.IsConverter( @@ -137,7 +88,7 @@ public class BlockListPropertyValueConverterTests } [Test] - public void Get_Value_Type_Single() + public void Get_Value_TypeSingle() { var editor = CreateConverter(); var config = ConfigForSingle(); @@ -152,7 +103,7 @@ public class BlockListPropertyValueConverterTests } [Test] - public void Get_Value_Type_SingleBlockMode() + public void Get_Value_TypeSingleBlockMode() { var editor = CreateConverter(); var config = ConfigForSingleBlockMode(); @@ -264,7 +215,7 @@ public class BlockListPropertyValueConverterTests }, ""contentData"": [ { - ""contentTypeKey"": """ + _contentKey1 + @""", + ""contentTypeKey"": """ + ContentKey1 + @""", ""key"": ""1304E1DD-0000-4396-84FE-8A399231CB3D"" } ] @@ -295,7 +246,7 @@ public class BlockListPropertyValueConverterTests }, ""contentData"": [ { - ""contentTypeKey"": """ + _contentKey1 + @""", + ""contentTypeKey"": """ + ContentKey1 + @""", ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" } ] @@ -337,29 +288,29 @@ public class BlockListPropertyValueConverterTests }, ""contentData"": [ { - ""contentTypeKey"": """ + _contentKey1 + @""", + ""contentTypeKey"": """ + ContentKey1 + @""", ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" }, { - ""contentTypeKey"": """ + _contentKey2 + @""", + ""contentTypeKey"": """ + ContentKey2 + @""", ""udi"": ""umb://element/E05A034704424AB3A520E048E6197E79"" }, { - ""contentTypeKey"": """ + _contentKey2 + @""", + ""contentTypeKey"": """ + ContentKey2 + @""", ""udi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"" } ], ""settingsData"": [ { - ""contentTypeKey"": """ + _settingKey1 + @""", + ""contentTypeKey"": """ + SettingKey1 + @""", ""udi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" }, { - ""contentTypeKey"": """ + _settingKey2 + @""", + ""contentTypeKey"": """ + SettingKey2 + @""", ""udi"": ""umb://element/1F613E26CE274898908A561437AF5100"" }, { - ""contentTypeKey"": """ + _settingKey2 + @""", + ""contentTypeKey"": """ + SettingKey2 + @""", ""udi"": ""umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9"" } ] @@ -386,7 +337,7 @@ public class BlockListPropertyValueConverterTests } [Test] - public void Data_Item_Removed_If_Removed_From_Config() + public void Data_Item_Removed_If_Removed_FromConfig() { var editor = CreateConverter(); @@ -398,7 +349,7 @@ public class BlockListPropertyValueConverterTests { new BlockListConfiguration.BlockConfiguration { - ContentElementTypeKey = _contentKey2, + ContentElementTypeKey = ContentKey2, SettingsElementTypeKey = null, }, }, @@ -423,29 +374,29 @@ public class BlockListPropertyValueConverterTests }, ""contentData"": [ { - ""contentTypeKey"": """ + _contentKey1 + @""", + ""contentTypeKey"": """ + ContentKey1 + @""", ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" }, { - ""contentTypeKey"": """ + _contentKey2 + @""", + ""contentTypeKey"": """ + ContentKey2 + @""", ""udi"": ""umb://element/E05A034704424AB3A520E048E6197E79"" }, { - ""contentTypeKey"": """ + _contentKey2 + @""", + ""contentTypeKey"": """ + ContentKey2 + @""", ""udi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"" } ], ""settingsData"": [ { - ""contentTypeKey"": """ + _settingKey1 + @""", + ""contentTypeKey"": """ + SettingKey1 + @""", ""udi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" }, { - ""contentTypeKey"": """ + _settingKey2 + @""", + ""contentTypeKey"": """ + SettingKey2 + @""", ""udi"": ""umb://element/1F613E26CE274898908A561437AF5100"" }, { - ""contentTypeKey"": """ + _settingKey2 + @""", + ""contentTypeKey"": """ + SettingKey2 + @""", ""udi"": ""umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9"" } ] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs new file mode 100644 index 0000000000..232b30da26 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs @@ -0,0 +1,64 @@ +using Moq; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +public abstract class BlockPropertyValueConverterTestsBase +{ + protected abstract string PropertyEditorAlias { get; } + + protected const string ContentAlias1 = "Test1"; + protected const string ContentAlias2 = "Test2"; + protected const string SettingAlias1 = "Setting1"; + protected const string SettingAlias2 = "Setting2"; + + protected Guid ContentKey1 { get; } = Guid.NewGuid(); + + protected Guid ContentKey2 { get; } = Guid.NewGuid(); + + protected Guid SettingKey1 { get; } = Guid.NewGuid(); + + protected Guid SettingKey2 { get; } = Guid.NewGuid(); + + /// + /// Setup mocks for IPublishedSnapshotAccessor + /// + protected IPublishedSnapshotAccessor GetPublishedSnapshotAccessor() + { + var test1ContentType = Mock.Of(x => + x.IsElement == true + && x.Key == ContentKey1 + && x.Alias == ContentAlias1); + var test2ContentType = Mock.Of(x => + x.IsElement == true + && x.Key == ContentKey2 + && x.Alias == ContentAlias2); + var test3ContentType = Mock.Of(x => + x.IsElement == true + && x.Key == SettingKey1 + && x.Alias == SettingAlias1); + var test4ContentType = Mock.Of(x => + x.IsElement == true + && x.Key == SettingKey2 + && x.Alias == SettingAlias2); + var contentCache = new Mock(); + contentCache.Setup(x => x.GetContentType(ContentKey1)).Returns(test1ContentType); + contentCache.Setup(x => x.GetContentType(ContentKey2)).Returns(test2ContentType); + contentCache.Setup(x => x.GetContentType(SettingKey1)).Returns(test3ContentType); + contentCache.Setup(x => x.GetContentType(SettingKey2)).Returns(test4ContentType); + var publishedSnapshot = Mock.Of(x => x.Content == contentCache.Object); + var publishedSnapshotAccessor = + Mock.Of(x => x.TryGetPublishedSnapshot(out publishedSnapshot)); + return publishedSnapshotAccessor; + } + + protected IPublishedPropertyType GetPropertyType(TPropertyEditorConfig config) + { + var dataType = new PublishedDataType(1, "test", new Lazy(() => config)); + var propertyType = Mock.Of(x => + x.EditorAlias == PropertyEditorAlias + && x.DataType == dataType); + return propertyType; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs new file mode 100644 index 0000000000..120254a305 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs @@ -0,0 +1,178 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class RichTextPropertyEditorHelperTests +{ + [Test] + public void Can_Parse_Pure_Markup_String() + { + var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue("

    this is some markup

    ", JsonSerializer(), Logger(), out RichTextEditorValue? value); + Assert.IsTrue(result); + Assert.IsNotNull(value); + Assert.AreEqual("

    this is some markup

    ", value.Markup); + Assert.IsNull(value.Blocks); + } + + [Test] + public void Can_Parse_JObject() + { + var input = JObject.Parse("""" + { + "markup": "

    this is some markup

    ", + "blocks": { + "layout": { + "Umbraco.TinyMCE": [{ + "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", + "settingsUdi": "umb://element/d2eeef66411142f4a1647a523eaffbc2", + } + ] + }, + "contentData": [{ + "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", + "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", + "contentPropertyAlias": "A content property value" + } + ], + "settingsData": [{ + "contentTypeKey": "e7a9447f-e14d-44dd-9ae8-e68c3c3da598", + "udi": "umb://element/d2eeef66411142f4a1647a523eaffbc2", + "settingsPropertyAlias": "A settings property value" + } + ] + } + } + """"); + + var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); + Assert.IsTrue(result); + Assert.IsNotNull(value); + Assert.AreEqual("

    this is some markup

    ", value.Markup); + + Assert.IsNotNull(value.Blocks); + + Assert.AreEqual(1, value.Blocks.ContentData.Count); + var item = value.Blocks.ContentData.Single(); + var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); + var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); + Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); + Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); + Assert.AreEqual(itemGuid, item.Key); + + Assert.AreEqual(1, value.Blocks.SettingsData.Count); + item = value.Blocks.SettingsData.Single(); + contentTypeGuid = Guid.Parse("e7a9447f-e14d-44dd-9ae8-e68c3c3da598"); + itemGuid = Guid.Parse("d2eeef66-4111-42f4-a164-7a523eaffbc2"); + Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); + Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); + Assert.AreEqual(itemGuid, item.Key); + } + + [Test] + public void Can_Parse_Blocks_With_Both_Content_And_Settings() + { + const string input = """ + { + "markup": "

    this is some markup

    ", + "blocks": { + "layout": { + "Umbraco.TinyMCE": [{ + "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", + "settingsUdi": "umb://element/d2eeef66411142f4a1647a523eaffbc2", + } + ] + }, + "contentData": [{ + "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", + "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", + "contentPropertyAlias": "A content property value" + } + ], + "settingsData": [{ + "contentTypeKey": "e7a9447f-e14d-44dd-9ae8-e68c3c3da598", + "udi": "umb://element/d2eeef66411142f4a1647a523eaffbc2", + "settingsPropertyAlias": "A settings property value" + } + ] + } + } + """; + + var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); + Assert.IsTrue(result); + Assert.IsNotNull(value); + Assert.AreEqual("

    this is some markup

    ", value.Markup); + + Assert.IsNotNull(value.Blocks); + + Assert.AreEqual(1, value.Blocks.ContentData.Count); + var item = value.Blocks.ContentData.Single(); + var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); + var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); + Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); + Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); + Assert.AreEqual(itemGuid, item.Key); + + Assert.AreEqual(1, value.Blocks.SettingsData.Count); + item = value.Blocks.SettingsData.Single(); + contentTypeGuid = Guid.Parse("e7a9447f-e14d-44dd-9ae8-e68c3c3da598"); + itemGuid = Guid.Parse("d2eeef66-4111-42f4-a164-7a523eaffbc2"); + Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); + Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); + Assert.AreEqual(itemGuid, item.Key); + } + + [Test] + public void Can_Parse_Blocks_With_Content_Only() + { + const string input = """ + { + "markup": "

    this is some markup

    ", + "blocks": { + "layout": { + "Umbraco.TinyMCE": [{ + "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02" + } + ] + }, + "contentData": [{ + "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", + "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", + "contentPropertyAlias": "A content property value" + } + ], + "settingsData": [] + } + } + """; + + var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); + Assert.IsTrue(result); + Assert.IsNotNull(value); + Assert.AreEqual("

    this is some markup

    ", value.Markup); + + Assert.IsNotNull(value.Blocks); + + Assert.AreEqual(1, value.Blocks.ContentData.Count); + var item = value.Blocks.ContentData.Single(); + var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); + var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); + Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); + Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); + Assert.AreEqual(itemGuid, item.Key); + + Assert.AreEqual(0, value.Blocks.SettingsData.Count); + } + + private IJsonSerializer JsonSerializer() => new JsonNetSerializer(); + + private ILogger Logger() => Mock.Of(); +} diff --git a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs index 3ef955afb8..a289a45ec3 100644 --- a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs +++ b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs @@ -80,5 +80,7 @@ internal class UmbracoCmsSchema public DataTypesSettings DataTypes { get; set; } = null!; public MarketplaceSettings Marketplace { get; set; } = null!; + + public WebhookSettings Webhook { get; set; } = null!; } }