From bde88caf14e30fb5ff8547c629aa6133a7d07338 Mon Sep 17 00:00:00 2001 From: Lucas Bach Bisgaard Date: Tue, 17 Oct 2023 13:24:24 +0200 Subject: [PATCH 01/20] Fixes #14351 - Using Fallback to default langauge on a specific item changes the whole VariationContext (#14620) (cherry picked from commit 738749b705d8ea99fd5c8de88a25cd0888ddf1c2) --- .../Extensions/PublishedElementExtensions.cs | 23 ------------------- .../IPublishedValueFallback.cs | 1 + .../PublishedValueFallback.cs | 1 + 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs b/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs index 440962cd76..c85178c85c 100644 --- a/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedElementExtensions.cs @@ -134,27 +134,6 @@ public static class PublishedElementExtensions #endregion - #region CheckVariation - /// - /// Method to check if VariationContext culture differs from culture parameter, if so it will update the VariationContext for the PublishedValueFallback. - /// - /// The requested PublishedValueFallback. - /// The requested culture. - /// The requested segment. - /// - private static void EventuallyUpdateVariationContext(IPublishedValueFallback publishedValueFallback, string? culture, string? segment) - { - IVariationContextAccessor? variationContextAccessor = publishedValueFallback.VariationContextAccessor; - - //If there is a difference in requested culture and the culture that is set in the VariationContext, it will pick wrong localized content. - //This happens for example using links to localized content in a RichText Editor. - if (!string.IsNullOrEmpty(culture) && variationContextAccessor?.VariationContext?.Culture != culture) - { - variationContextAccessor!.VariationContext = new VariationContext(culture, segment); - } - } - #endregion - #region Value /// @@ -195,8 +174,6 @@ public static class PublishedElementExtensions { IPublishedProperty? property = content.GetProperty(alias); - EventuallyUpdateVariationContext(publishedValueFallback, culture, segment); - // if we have a property, and it has a value, return that value if (property != null && property.HasValue(culture, segment)) { diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs index 111d747ec1..839b73ea51 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedValueFallback.cs @@ -5,6 +5,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent; /// public interface IPublishedValueFallback { + [Obsolete("Scheduled for removal in v14")] /// /// VariationContextAccessor that is not required to be implemented, therefore throws NotImplementedException as default. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs index d505c5f2c3..16c648233d 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs @@ -20,6 +20,7 @@ public class PublishedValueFallback : IPublishedValueFallback _variationContextAccessor = variationContextAccessor; } + [Obsolete("Scheduled for removal in v14")] public IVariationContextAccessor VariationContextAccessor { get { return _variationContextAccessor; } } /// From 761088c0e9740134fb94c7674caf00f5eb701583 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Oct 2023 08:59:38 +0100 Subject: [PATCH 02/20] Update version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 4e0b8cb597..e4ab52430e 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.0.0-rc", + "version": "13.0.0-rc1", "assemblyVersion": { "precision": "build" }, From 60e910e7e581ca1af9888c278d28f1935d8d4d1c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Oct 2023 12:31:17 +0100 Subject: [PATCH 03/20] Remove the parts later moved to v14 --- .../Constants-JsonOptionsNames.cs | 15 -- .../Constants-OauthClientIds.cs | 23 --- .../Factories/IDatabaseSettingsFactory.cs | 16 -- .../Factories/IInstallSettingsFactory.cs | 8 - .../Factories/IUpgradeSettingsFactory.cs | 8 - .../Factories/IUserSettingsFactory.cs | 8 - .../Factories/InstallSettingsFactory.cs | 24 --- .../Factories/UpgradeSettingsFactory.cs | 34 ---- .../Factories/UserSettingsFactory.cs | 53 ------ .../Installer/IInstallStep.cs | 23 --- .../Installer/IUpgradeStep.cs | 18 -- .../Installer/NewInstallStepCollection.cs | 12 -- .../NewInstallStepCollectionBuilder.cs | 11 -- .../Installer/Steps/FilePermissionsStep.cs | 47 ----- .../Installer/Steps/RestartRuntimeStep.cs | 23 --- .../Steps/TelemetryIdentifierStep.cs | 45 ----- .../Installer/UpgradeStepCollection.cs | 11 -- .../Installer/UpgradeStepCollectionBuilder.cs | 11 -- .../Configuration/NewBackOfficeSettings.cs | 10 - .../NewBackOfficeSettingsValidator.cs | 25 --- .../Models/Installer/ConsentLevelModel.cs | 10 - .../Models/Installer/DatabaseInstallData.cs | 20 -- .../Models/Installer/DatabaseSettingsModel.cs | 26 --- .../Models/Installer/InstallData.cs | 12 -- .../Models/Installer/InstallSettingsModel.cs | 8 - .../Models/Installer/PasswordSettingsModel.cs | 8 - .../Models/Installer/UpgradeSettingsModel.cs | 15 -- .../Models/Installer/UserInstallData.cs | 12 -- .../Models/Installer/UserSettingsModel.cs | 8 - .../Models/OutOfDateType.cs | 8 - .../RedirectUrlManagement/RedirectStatus.cs | 7 - .../Services/Installer/IInstallService.cs | 14 -- .../Services/Installer/ILanguageService.cs | 11 -- .../Services/Installer/IUpgradeService.cs | 11 -- .../Services/Installer/InstallService.cs | 61 ------ .../Services/Installer/UpgradeService.cs | 61 ------ .../Services/Languages/LanguageService.cs | 92 --------- .../Umbraco.New.Cms.Core.csproj | 12 -- .../Installer/DatabaseSettingsFactory.cs | 54 ------ .../HostedServices/OpenIddictCleanup.cs | 56 ------ .../Installer/Steps/CreateUserStep.cs | 176 ------------------ .../Installer/Steps/DatabaseConfigureStep.cs | 68 ------- .../Installer/Steps/DatabaseInstallStep.cs | 49 ----- .../Installer/Steps/DatabaseUpgradeStep.cs | 79 -------- .../Steps/RegisterInstallCompleteStep.cs | 24 --- .../Security/IBackOfficeApplicationManager.cs | 6 - .../Services/IIndexingRebuilderService.cs | 11 -- .../Services/IndexingRebuilderService.cs | 86 --------- .../Umbraco.New.Cms.Infrastructure.csproj | 17 -- .../Installer/SignInUserStep.cs | 34 ---- .../Umbraco.New.Cms.Web.Common.csproj | 13 -- 51 files changed, 1494 deletions(-) delete mode 100644 src/Umbraco.New.Cms.Core/Constants-JsonOptionsNames.cs delete mode 100644 src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs delete mode 100644 src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs delete mode 100644 src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs delete mode 100644 src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs delete mode 100644 src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs delete mode 100644 src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs delete mode 100644 src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs delete mode 100644 src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs delete mode 100644 src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/OutOfDateType.cs delete mode 100644 src/Umbraco.New.Cms.Core/Models/RedirectUrlManagement/RedirectStatus.cs delete mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs delete mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/ILanguageService.cs delete mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs delete mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs delete mode 100644 src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs delete mode 100644 src/Umbraco.New.Cms.Core/Services/Languages/LanguageService.cs delete mode 100644 src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/HostedServices/OpenIddictCleanup.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Security/IBackOfficeApplicationManager.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Services/IIndexingRebuilderService.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Services/IndexingRebuilderService.cs delete mode 100644 src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj delete mode 100644 src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs delete mode 100644 src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj diff --git a/src/Umbraco.New.Cms.Core/Constants-JsonOptionsNames.cs b/src/Umbraco.New.Cms.Core/Constants-JsonOptionsNames.cs deleted file mode 100644 index e7841c1607..0000000000 --- a/src/Umbraco.New.Cms.Core/Constants-JsonOptionsNames.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.New.Cms.Core; - -public static partial class Constants -{ - // TODO: move this class to Umbraco.Cms.Core as a partial class - public static partial class JsonOptionsNames - { - /// - /// Name used for JsonOptions - /// - public const string BackOffice = "BackOffice"; - } -} - - diff --git a/src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs b/src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs deleted file mode 100644 index 02f190f0ab..0000000000 --- a/src/Umbraco.New.Cms.Core/Constants-OauthClientIds.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Umbraco.New.Cms.Core; - -// TODO: move this class to Umbraco.Cms.Core as a partial class -public static partial class Constants -{ - public static partial class OauthClientIds - { - /// - /// Client ID used for default back-office access - /// - public const string BackOffice = "umbraco-back-office"; - - /// - /// Client ID used for Swagger API access - /// - public const string Swagger = "umbraco-swagger"; - - /// - /// Client ID used for Postman API access - /// - public const string Postman = "umbraco-postman"; - } -} diff --git a/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs deleted file mode 100644 index c71ce126d8..0000000000 --- a/src/Umbraco.New.Cms.Core/Factories/IDatabaseSettingsFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Factories; - -/// -/// Creates based on the currently configured providers. -/// -public interface IDatabaseSettingsFactory -{ - /// - /// Creates a collection of database settings models for the currently installed database providers - /// - /// Collection of database settings. - /// Thrown if a connection string is preconfigured, but provider name is missing. - ICollection GetDatabaseSettings(); -} diff --git a/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs deleted file mode 100644 index 552cd0af2b..0000000000 --- a/src/Umbraco.New.Cms.Core/Factories/IInstallSettingsFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Factories; - -public interface IInstallSettingsFactory -{ - InstallSettingsModel GetInstallSettings(); -} diff --git a/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs deleted file mode 100644 index 45daf3dcc1..0000000000 --- a/src/Umbraco.New.Cms.Core/Factories/IUpgradeSettingsFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Factories; - -public interface IUpgradeSettingsFactory -{ - UpgradeSettingsModel GetUpgradeSettings(); -} diff --git a/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs deleted file mode 100644 index 5c069d7084..0000000000 --- a/src/Umbraco.New.Cms.Core/Factories/IUserSettingsFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Factories; - -public interface IUserSettingsFactory -{ - UserSettingsModel GetUserSettings(); -} diff --git a/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs deleted file mode 100644 index 5d57b1554d..0000000000 --- a/src/Umbraco.New.Cms.Core/Factories/InstallSettingsFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Factories; - -public class InstallSettingsFactory : IInstallSettingsFactory -{ - private readonly IUserSettingsFactory _userSettingsFactory; - private readonly IDatabaseSettingsFactory _databaseSettingsFactory; - - public InstallSettingsFactory( - IUserSettingsFactory userSettingsFactory, - IDatabaseSettingsFactory databaseSettingsFactory) - { - _userSettingsFactory = userSettingsFactory; - _databaseSettingsFactory = databaseSettingsFactory; - } - - public InstallSettingsModel GetInstallSettings() => - new() - { - DatabaseSettings = _databaseSettingsFactory.GetDatabaseSettings(), - UserSettings = _userSettingsFactory.GetUserSettings(), - }; -} diff --git a/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs deleted file mode 100644 index 314e83a995..0000000000 --- a/src/Umbraco.New.Cms.Core/Factories/UpgradeSettingsFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Semver; -using Umbraco.Cms.Core.Services; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Factories; - -public class UpgradeSettingsFactory : IUpgradeSettingsFactory -{ - private readonly IRuntimeState _runtimeState; - private readonly IUmbracoVersion _umbracoVersion; - - public UpgradeSettingsFactory( - IRuntimeState runtimeState, - IUmbracoVersion umbracoVersion) - { - _runtimeState = runtimeState; - _umbracoVersion = umbracoVersion; - } - - - public UpgradeSettingsModel GetUpgradeSettings() - { - var model = new UpgradeSettingsModel - { - CurrentState = _runtimeState.CurrentMigrationState ?? string.Empty, - NewState = _runtimeState.FinalMigrationState ?? string.Empty, - NewVersion = _umbracoVersion.SemanticVersion, - OldVersion = new SemVersion(_umbracoVersion.SemanticVersion.Major), // TODO can we find the old version somehow? e.g. from current state - }; - - return model; - } -} diff --git a/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs b/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs deleted file mode 100644 index 9386cf713a..0000000000 --- a/src/Umbraco.New.Cms.Core/Factories/UserSettingsFactory.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Services; -using Umbraco.Extensions; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Factories; - -public class UserSettingsFactory : IUserSettingsFactory -{ - private readonly ILocalizedTextService _localizedTextService; - private readonly UserPasswordConfigurationSettings _passwordConfiguration; - - public UserSettingsFactory( - IOptions securitySettings, - ILocalizedTextService localizedTextService) - { - _localizedTextService = localizedTextService; - _passwordConfiguration = securitySettings.Value; - } - - public UserSettingsModel GetUserSettings() => - new() - { - PasswordSettings = CreatePasswordSettingsModel(), - ConsentLevels = CreateConsentLevelModels(), - }; - - private PasswordSettingsModel CreatePasswordSettingsModel() => - new() - { - MinCharLength = _passwordConfiguration.RequiredLength, - MinNonAlphaNumericLength = _passwordConfiguration.GetMinNonAlphaNumericChars() - }; - - private IEnumerable CreateConsentLevelModels() => - Enum.GetValues() - .ToList() - .Select(level => new ConsentLevelModel - { - Level = level, - Description = GetTelemetryLevelDescription(level), - }); - - private string GetTelemetryLevelDescription(TelemetryLevel telemetryLevel) => telemetryLevel switch - { - TelemetryLevel.Minimal => _localizedTextService.Localize("analytics", "minimalLevelDescription"), - TelemetryLevel.Basic => _localizedTextService.Localize("analytics", "basicLevelDescription"), - TelemetryLevel.Detailed => _localizedTextService.Localize("analytics", "detailedLevelDescription"), - _ => throw new ArgumentOutOfRangeException(nameof(telemetryLevel), $"Did not expect telemetry level of {telemetryLevel}") - }; -} diff --git a/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs b/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs deleted file mode 100644 index 3344b9ffb0..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/IInstallStep.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Installer; - -/// -/// Defines a step that's required to install Umbraco. -/// -public interface IInstallStep -{ - /// - /// Executes the install step. - /// - /// InstallData model containing the data provided by the installer UI. - /// - Task ExecuteAsync(InstallData model); - - /// - /// Determines if the step is required to execute. - /// - /// InstallData model containing the data provided by the installer UI. - /// True if the step should execute, otherwise false. - Task RequiresExecutionAsync(InstallData model); -} diff --git a/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs b/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs deleted file mode 100644 index 6f52aca6ec..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/IUpgradeStep.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Umbraco.New.Cms.Core.Installer; - -/// -/// Defines a step that's required to upgrade Umbraco. -/// -public interface IUpgradeStep -{ - /// - /// Executes the upgrade step. - /// - Task ExecuteAsync(); - - /// - /// Determines if the step is required to execute. - /// - /// True if the step should execute, otherwise false. - Task RequiresExecutionAsync(); -} diff --git a/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs deleted file mode 100644 index 7077b85a28..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Umbraco.Cms.Core.Composing; -using Umbraco.Cms.Core.Install.Models; - -namespace Umbraco.New.Cms.Core.Installer; - -public class NewInstallStepCollection : BuilderCollectionBase -{ - public NewInstallStepCollection(Func> items) - : base(items) - { - } -} diff --git a/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs b/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs deleted file mode 100644 index d3c572b7b7..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/NewInstallStepCollectionBuilder.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Composing; - -namespace Umbraco.New.Cms.Core.Installer; - -public class NewInstallStepCollectionBuilder : OrderedCollectionBuilderBase -{ - protected override NewInstallStepCollectionBuilder This => this; - - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; -} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs deleted file mode 100644 index 37574c91e1..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/Steps/FilePermissionsStep.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Umbraco.Cms.Core.Install; -using Umbraco.Cms.Core.Services; -using Umbraco.Extensions; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Installer.Steps; - -public class FilePermissionsStep : IInstallStep, IUpgradeStep -{ - private readonly IFilePermissionHelper _filePermissionHelper; - private readonly ILocalizedTextService _localizedTextService; - - public FilePermissionsStep( - IFilePermissionHelper filePermissionHelper, - ILocalizedTextService localizedTextService) - { - _filePermissionHelper = filePermissionHelper; - _localizedTextService = localizedTextService; - } - - public Task ExecuteAsync(InstallData _) => Execute(); - - public Task ExecuteAsync() => Execute(); - - private Task Execute() - { - // validate file permissions - var permissionsOk = - _filePermissionHelper.RunFilePermissionTestSuite( - out Dictionary> report); - - var translatedErrors = - report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value); - if (permissionsOk == false) - { - throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors }); - } - - return Task.CompletedTask; - } - - public Task RequiresExecutionAsync(InstallData model) => ShouldExecute(); - - public Task RequiresExecutionAsync() => ShouldExecute(); - - private static Task ShouldExecute() => Task.FromResult(true); -} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs deleted file mode 100644 index cacce0d763..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/Steps/RestartRuntimeStep.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Umbraco.Cms.Core.Services; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Installer.Steps; - -public class RestartRuntimeStep : IInstallStep, IUpgradeStep -{ - private readonly IRuntime _runtime; - - public RestartRuntimeStep(IRuntime runtime) => _runtime = runtime; - - public async Task ExecuteAsync(InstallData _) => await Execute(); - - public async Task ExecuteAsync() => await Execute(); - - private async Task Execute() => await _runtime.RestartAsync(); - - public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); - - public Task RequiresExecutionAsync() => ShouldExecute(); - - private Task ShouldExecute() => Task.FromResult(true); -} diff --git a/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs b/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs deleted file mode 100644 index a05a8228fe..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/Steps/TelemetryIdentifierStep.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Install.Models; -using Umbraco.Cms.Core.Telemetry; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Installer.Steps; - -public class TelemetryIdentifierStep : IInstallStep, IUpgradeStep -{ - private readonly IOptions _globalSettings; - private readonly ISiteIdentifierService _siteIdentifierService; - - public TelemetryIdentifierStep( - IOptions globalSettings, - ISiteIdentifierService siteIdentifierService) - { - _globalSettings = globalSettings; - _siteIdentifierService = siteIdentifierService; - } - - public Task ExecuteAsync(InstallData _) => Execute(); - - public Task ExecuteAsync() => Execute(); - - private Task Execute() - { - _siteIdentifierService.TryCreateSiteIdentifier(out _); - return Task.CompletedTask; - } - - public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); - - public Task RequiresExecutionAsync() => ShouldExecute(); - - private Task ShouldExecute() - { - // Verify that Json value is not empty string - // Try & get a value stored in appSettings.json - var backofficeIdentifierRaw = _globalSettings.Value.Id; - - // No need to add Id again if already found - return Task.FromResult(string.IsNullOrEmpty(backofficeIdentifierRaw)); - } -} diff --git a/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs deleted file mode 100644 index 1deb06681d..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollection.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Umbraco.Cms.Core.Composing; - -namespace Umbraco.New.Cms.Core.Installer; - -public class UpgradeStepCollection : BuilderCollectionBase -{ - public UpgradeStepCollection(Func> items) - : base(items) - { - } -} diff --git a/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs b/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs deleted file mode 100644 index a7b2b803ce..0000000000 --- a/src/Umbraco.New.Cms.Core/Installer/UpgradeStepCollectionBuilder.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Composing; - -namespace Umbraco.New.Cms.Core.Installer; - -public class UpgradeStepCollectionBuilder : OrderedCollectionBuilderBase -{ - protected override UpgradeStepCollectionBuilder This => this; - - protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Scoped; -} diff --git a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs deleted file mode 100644 index cad7b8868d..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Umbraco.Cms.Core.Configuration.Models; - -namespace Umbraco.New.Cms.Core.Models.Configuration; - -// TODO: merge this class with relevant existing settings from Core and clean up -[UmbracoOptions($"{Umbraco.Cms.Core.Constants.Configuration.ConfigPrefix}NewBackOffice")] -public class NewBackOfficeSettings -{ - public Uri? BackOfficeHost { get; set; } = null; -} diff --git a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs b/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs deleted file mode 100644 index bb1a2eda3d..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Configuration/NewBackOfficeSettingsValidator.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration.Models.Validation; - -namespace Umbraco.New.Cms.Core.Models.Configuration; - -// TODO: merge this class with relevant existing settings validators from Core and clean up -public class NewBackOfficeSettingsValidator : ConfigurationValidatorBase, IValidateOptions -{ - public ValidateOptionsResult Validate(string? name, NewBackOfficeSettings options) - { - if (options.BackOfficeHost != null) - { - if (options.BackOfficeHost.IsAbsoluteUri == false) - { - return ValidateOptionsResult.Fail($"{nameof(NewBackOfficeSettings.BackOfficeHost)} must be an absolute URL"); - } - if (options.BackOfficeHost.PathAndQuery != "/") - { - return ValidateOptionsResult.Fail($"{nameof(NewBackOfficeSettings.BackOfficeHost)} must not have any path or query"); - } - } - - return ValidateOptionsResult.Success; - } -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs deleted file mode 100644 index a3687814c3..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/ConsentLevelModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Umbraco.Cms.Core.Models; - -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class ConsentLevelModel -{ - public TelemetryLevel Level { get; set; } - - public string Description { get; set; } = string.Empty; -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs deleted file mode 100644 index 6141ea7a9f..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseInstallData.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class DatabaseInstallData -{ - public Guid Id { get; set; } - - public string? ProviderName { get; set; } - - public string? Server { get; set; } - - public string? Name { get; set; } - - public string? Username { get; set; } - - public string? Password { get; set; } - - public bool UseIntegratedAuthentication { get; set; } - - public string? ConnectionString { get; set; } -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs deleted file mode 100644 index 2f8aabb8af..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/DatabaseSettingsModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class DatabaseSettingsModel -{ - public Guid Id { get; set; } - - public int SortOrder { get; set; } - - public string DisplayName { get; set; } = string.Empty; - - public string DefaultDatabaseName { get; set; } = string.Empty; - - public string ProviderName { get; set; } = string.Empty; - - public bool IsConfigured { get; set; } - - public bool RequiresServer { get; set; } - - public string ServerPlaceholder { get; set; } = string.Empty; - - public bool RequiresCredentials { get; set; } - - public bool SupportsIntegratedAuthentication { get; set; } - - public bool RequiresConnectionTest { get; set; } -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs deleted file mode 100644 index 77d0c07477..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/InstallData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Umbraco.Cms.Core.Models; - -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class InstallData -{ - public UserInstallData User { get; set; } = null!; - - public DatabaseInstallData Database { get; set; } = null!; - - public TelemetryLevel TelemetryLevel { get; set; } = TelemetryLevel.Detailed; -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs deleted file mode 100644 index 6b0aeb370d..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/InstallSettingsModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class InstallSettingsModel -{ - public UserSettingsModel UserSettings { get; set; } = null!; - - public ICollection DatabaseSettings { get; set; } = new List(); -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs deleted file mode 100644 index 2efec3a696..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/PasswordSettingsModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class PasswordSettingsModel -{ - public int MinCharLength { get; set; } - - public int MinNonAlphaNumericLength { get; set; } -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs deleted file mode 100644 index b403367548..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/UpgradeSettingsModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Umbraco.Cms.Core.Semver; -using Umbraco.Extensions; - -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class UpgradeSettingsModel -{ - public string CurrentState { get; set; } = string.Empty; - - public string NewState { get; set; } = string.Empty; - - public SemVersion NewVersion { get; set; } = null!; - - public SemVersion OldVersion { get; set; } = null!; -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs deleted file mode 100644 index 18865565df..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/UserInstallData.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class UserInstallData -{ - public string Name { get; set; } = null!; - - public string Email { get; set; } = null!; - - public string Password { get; set; } = null!; - - public bool SubscribeToNewsletter { get; set; } -} diff --git a/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs b/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs deleted file mode 100644 index 2db9f04b65..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/Installer/UserSettingsModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.New.Cms.Core.Models.Installer; - -public class UserSettingsModel -{ - public PasswordSettingsModel PasswordSettings { get; set; } = null!; - - public IEnumerable ConsentLevels { get; set; } = Enumerable.Empty(); -} diff --git a/src/Umbraco.New.Cms.Core/Models/OutOfDateType.cs b/src/Umbraco.New.Cms.Core/Models/OutOfDateType.cs deleted file mode 100644 index 2210f02e77..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/OutOfDateType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.New.Cms.Core.Models; - -public enum OutOfDateType -{ - OutOfDate, - Current, - Unknown = 100 -} diff --git a/src/Umbraco.New.Cms.Core/Models/RedirectUrlManagement/RedirectStatus.cs b/src/Umbraco.New.Cms.Core/Models/RedirectUrlManagement/RedirectStatus.cs deleted file mode 100644 index 04fd70d7e5..0000000000 --- a/src/Umbraco.New.Cms.Core/Models/RedirectUrlManagement/RedirectStatus.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Umbraco.New.Cms.Core.Models.RedirectUrlManagement; - -public enum RedirectStatus -{ - Enabled, - Disabled, -} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs deleted file mode 100644 index c5dc499d62..0000000000 --- a/src/Umbraco.New.Cms.Core/Services/Installer/IInstallService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Umbraco.New.Cms.Core.Installer; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Services.Installer; - -public interface IInstallService -{ - /// - /// Runs all the steps in the , installing Umbraco - /// - /// InstallData containing the required data used to install - /// - Task Install(InstallData model); -} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/ILanguageService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/ILanguageService.cs deleted file mode 100644 index 29bb2df265..0000000000 --- a/src/Umbraco.New.Cms.Core/Services/Installer/ILanguageService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Umbraco.Cms.Core.Models; - -namespace Umbraco.New.Cms.Core.Services.Installer; - -public interface ILanguageService -{ - bool LanguageAlreadyExists(int id, string isoCode); - - bool CanUseLanguagesFallbackLanguage(ILanguage language); - bool CanGetProperFallbackLanguage(ILanguage existingById); -} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs deleted file mode 100644 index f6eefa8a85..0000000000 --- a/src/Umbraco.New.Cms.Core/Services/Installer/IUpgradeService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Umbraco.New.Cms.Core.Installer; - -namespace Umbraco.New.Cms.Core.Services.Installer; - -public interface IUpgradeService -{ - /// - /// Runs all the steps in the , upgrading Umbraco. - /// - Task Upgrade(); -} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs deleted file mode 100644 index 98813cdaec..0000000000 --- a/src/Umbraco.New.Cms.Core/Services/Installer/InstallService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Services; -using Umbraco.New.Cms.Core.Installer; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Core.Services.Installer; - -public class InstallService : IInstallService -{ - private readonly ILogger _logger; - private readonly NewInstallStepCollection _installSteps; - private readonly IRuntimeState _runtimeState; - - public InstallService( - ILogger logger, - NewInstallStepCollection installSteps, - IRuntimeState runtimeState) - { - _logger = logger; - _installSteps = installSteps; - _runtimeState = runtimeState; - } - - /// - public async Task Install(InstallData model) - { - if (_runtimeState.Level != RuntimeLevel.Install) - { - throw new InvalidOperationException($"Runtime level must be Install to install but was: {_runtimeState.Level}"); - } - - try - { - await RunSteps(model); - } - catch (Exception exception) - { - _logger.LogError(exception, "Encountered an error when running the install steps"); - throw; - } - } - - private async Task RunSteps(InstallData model) - { - foreach (IInstallStep step in _installSteps) - { - var stepName = step.GetType().Name; - _logger.LogInformation("Checking if {StepName} requires execution", stepName); - if (await step.RequiresExecutionAsync(model) is false) - { - _logger.LogInformation("Skipping {StepName}", stepName); - continue; - } - - _logger.LogInformation("Running {StepName}", stepName); - await step.ExecuteAsync(model); - _logger.LogInformation("Finished {StepName}", stepName); - } - } -} diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs deleted file mode 100644 index 6f11e8a7ac..0000000000 --- a/src/Umbraco.New.Cms.Core/Services/Installer/UpgradeService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Services; -using Umbraco.New.Cms.Core.Installer; - -namespace Umbraco.New.Cms.Core.Services.Installer; - -public class UpgradeService : IUpgradeService -{ - private readonly UpgradeStepCollection _upgradeSteps; - private readonly IRuntimeState _runtimeState; - private readonly ILogger _logger; - - public UpgradeService( - UpgradeStepCollection upgradeSteps, - IRuntimeState runtimeState, - ILogger logger) - { - _upgradeSteps = upgradeSteps; - _runtimeState = runtimeState; - _logger = logger; - } - - /// - public async Task Upgrade() - { - if (_runtimeState.Level != RuntimeLevel.Upgrade) - { - throw new InvalidOperationException( - $"Runtime level must be Upgrade to upgrade but was: {_runtimeState.Level}"); - } - - try - { - await RunSteps(); - } - catch (Exception exception) - { - _logger.LogError(exception, "Encountered an error when running the upgrade steps"); - throw; - } - } - - private async Task RunSteps() - { - foreach (IUpgradeStep step in _upgradeSteps) - { - var stepName = step.GetType().Name; - _logger.LogInformation("Checking if {StepName} requires execution", stepName); - if (await step.RequiresExecutionAsync() is false) - { - _logger.LogInformation("Skipping {StepName}", stepName); - continue; - } - - _logger.LogInformation("Running {StepName}", stepName); - await step.ExecuteAsync(); - _logger.LogInformation("Finished {StepName}", stepName); - } - } -} diff --git a/src/Umbraco.New.Cms.Core/Services/Languages/LanguageService.cs b/src/Umbraco.New.Cms.Core/Services/Languages/LanguageService.cs deleted file mode 100644 index 73a4ded3d5..0000000000 --- a/src/Umbraco.New.Cms.Core/Services/Languages/LanguageService.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Services; -using Umbraco.New.Cms.Core.Services.Installer; - -namespace Umbraco.New.Cms.Core.Services.Languages; - -public class LanguageService : ILanguageService -{ - private readonly ILocalizationService _localizationService; - - public LanguageService(ILocalizationService localizationService) - { - _localizationService = localizationService; - } - - public bool LanguageAlreadyExists(int id, string isoCode) - { - // this is prone to race conditions but the service will not let us proceed anyways - ILanguage? existingByCulture = _localizationService.GetLanguageByIsoCode(isoCode); - - // the localization service might return the generic language even when queried for specific ones (e.g. "da" when queried for "da-DK") - // - we need to handle that explicitly - if (existingByCulture?.IsoCode != isoCode) - { - existingByCulture = null; - } - - if (existingByCulture != null && id != existingByCulture.Id) - { - return true; - } - - ILanguage? existingById = id != default ? _localizationService.GetLanguageById(id) : null; - return existingById is not null; - } - - public bool CanUseLanguagesFallbackLanguage(ILanguage language) - { - if (!language.FallbackLanguageId.HasValue) - { - return false; - } - - var languages = _localizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); - return languages.ContainsKey(language.FallbackLanguageId.Value); - - } - - public bool CanGetProperFallbackLanguage(ILanguage existingById) - { - // modifying an existing language can create a fallback, verify - // note that the service will check again, dealing with race conditions - if (existingById.FallbackLanguageId.HasValue) - { - var languages = _localizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); - - if (CreatesCycle(existingById, languages)) - { - return false; - } - } - - return true; - } - - - // see LocalizationService - private bool CreatesCycle(ILanguage language, IDictionary languages) - { - // a new language is not referenced yet, so cannot be part of a cycle - if (!language.HasIdentity) - { - return false; - } - - var id = language.FallbackLanguageId; - while (true) // assuming languages does not already contains a cycle, this must end - { - if (!id.HasValue) - { - return false; // no fallback means no cycle - } - - if (id.Value == language.Id) - { - return true; // back to language = cycle! - } - - id = languages[id.Value].FallbackLanguageId; // else keep chaining - } - } -} 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 339bff9be6..0000000000 --- a/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - Umbraco CMS - Core - Contains the core assembly needed to run Umbraco CMS. - false - false - - - - - - diff --git a/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs b/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs deleted file mode 100644 index be941104b6..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Factories/Installer/DatabaseSettingsFactory.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Mapping; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Extensions; -using Umbraco.New.Cms.Core.Factories; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Infrastructure.Factories.Installer; - -public class DatabaseSettingsFactory : IDatabaseSettingsFactory -{ - private readonly IEnumerable _databaseProviderMetadata; - private readonly IOptionsMonitor _connectionStrings; - private readonly IUmbracoMapper _mapper; - - public DatabaseSettingsFactory( - IEnumerable databaseProviderMetadata, - IOptionsMonitor connectionStrings, - IUmbracoMapper mapper) - { - _databaseProviderMetadata = databaseProviderMetadata; - _connectionStrings = connectionStrings; - _mapper = mapper; - } - - /// - public ICollection GetDatabaseSettings() - { - ConnectionStrings? connectionString = _connectionStrings.CurrentValue; - - // If the connection string is configured we just return the configured provider. - if (connectionString.IsConnectionStringConfigured()) - { - var providerName = connectionString.ProviderName; - IDatabaseProviderMetadata? providerMetaData = _databaseProviderMetadata - .FirstOrDefault(x => x.ProviderName?.Equals(providerName, StringComparison.InvariantCultureIgnoreCase) ?? false); - - if (providerMetaData is null) - { - throw new InvalidOperationException($"Provider {providerName} is not a registered provider"); - } - - DatabaseSettingsModel configuredProvider = _mapper.Map(providerMetaData)!; - - configuredProvider.IsConfigured = true; - - return new[] { configuredProvider }; - } - - List providers = _mapper.MapEnumerable(_databaseProviderMetadata); - return providers; - } -} diff --git a/src/Umbraco.New.Cms.Infrastructure/HostedServices/OpenIddictCleanup.cs b/src/Umbraco.New.Cms.Infrastructure/HostedServices/OpenIddictCleanup.cs deleted file mode 100644 index 37a6e4caa6..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/HostedServices/OpenIddictCleanup.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using OpenIddict.Abstractions; -using Umbraco.Cms.Infrastructure.HostedServices; - -namespace Umbraco.New.Cms.Infrastructure.HostedServices; - -// port of the OpenIddict Quartz job for cleaning up - see https://github.com/openiddict/openiddict-core/tree/dev/src/OpenIddict.Quartz -public class OpenIddictCleanup : RecurringHostedServiceBase -{ - // keep tokens and authorizations in the database for 7 days - // - NOTE: this is NOT the same as access token lifetime, which is likely very short - private const int LifespanInSeconds = 7 * 24 * 60 * 60; - - private readonly ILogger _logger; - private readonly IServiceProvider _provider; - - public OpenIddictCleanup( - ILogger logger, IServiceProvider provider) - : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(5)) - { - _logger = logger; - _provider = provider; - } - - public override async Task PerformExecuteAsync(object? state) - { - // hosted services are registered as singletons, but this particular one consumes scoped services... so - // we have to fetch the service dependencies manually using a new scope per invocation. - IServiceScope scope = _provider.CreateScope(); - DateTimeOffset threshold = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(LifespanInSeconds); - - try - { - IOpenIddictTokenManager tokenManager = scope.ServiceProvider.GetService() - ?? throw new ConfigurationErrorsException($"Could not retrieve an {nameof(IOpenIddictTokenManager)} service from the current scope"); - await tokenManager.PruneAsync(threshold); - } - catch (Exception exception) - { - _logger.LogError(exception, "Unable to prune OpenIddict tokens"); - } - - try - { - IOpenIddictAuthorizationManager authorizationManager = scope.ServiceProvider.GetService() - ?? throw new ConfigurationErrorsException($"Could not retrieve an {nameof(IOpenIddictAuthorizationManager)} service from the current scope"); - await authorizationManager.PruneAsync(threshold); - } - catch (Exception exception) - { - _logger.LogError(exception, "Unable to prune OpenIddict authorizations"); - } - } -} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs deleted file mode 100644 index 38faddff09..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/CreateUserStep.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System.Collections.Specialized; -using System.Data.Common; -using System.Text; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Install.Models; -using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Extensions; -using Umbraco.New.Cms.Core.Installer; -using Umbraco.New.Cms.Core.Models.Installer; -using Constants = Umbraco.Cms.Core.Constants; -using HttpResponseMessage = System.Net.Http.HttpResponseMessage; - -namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; - -public class CreateUserStep : IInstallStep -{ - private readonly IUserService _userService; - private readonly DatabaseBuilder _databaseBuilder; - private readonly IHttpClientFactory _httpClientFactory; - private readonly SecuritySettings _securitySettings; - private readonly IOptionsMonitor _connectionStrings; - private readonly ICookieManager _cookieManager; - private readonly IBackOfficeUserManager _userManager; - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly IMetricsConsentService _metricsConsentService; - - public CreateUserStep( - IUserService userService, - DatabaseBuilder databaseBuilder, - IHttpClientFactory httpClientFactory, - IOptions securitySettings, - IOptionsMonitor connectionStrings, - ICookieManager cookieManager, - IBackOfficeUserManager userManager, - IDbProviderFactoryCreator dbProviderFactoryCreator, - IMetricsConsentService metricsConsentService) - { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); - _httpClientFactory = httpClientFactory; - _securitySettings = securitySettings.Value ?? throw new ArgumentNullException(nameof(securitySettings)); - _connectionStrings = connectionStrings; - _cookieManager = cookieManager; - _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); - _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); - _metricsConsentService = metricsConsentService; - } - - public async Task ExecuteAsync(InstallData model) - { - IUser? admin = _userService.GetUserById(Constants.Security.SuperUserId); - if (admin == null) - { - throw new InvalidOperationException("Could not find the super user!"); - } - - UserInstallData user = model.User; - admin.Email = user.Email.Trim(); - admin.Name = user.Name.Trim(); - admin.Username = user.Email.Trim(); - - _userService.Save(admin); - - BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); - if (membershipUser == null) - { - throw new InvalidOperationException( - $"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}."); - } - - //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); - if (string.IsNullOrWhiteSpace(resetToken)) - { - throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); - } - - IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); - if (!resetResult.Succeeded) - { - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); - } - - _metricsConsentService.SetConsentLevel(model.TelemetryLevel); - - if (model.User.SubscribeToNewsletter) - { - var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email } }; - var content = new StringContent(JsonConvert.SerializeObject(values), Encoding.UTF8, "application/json"); - - HttpClient httpClient = _httpClientFactory.CreateClient(); - - try - { - HttpResponseMessage response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; - } - catch { /* fail in silence */ } - } - } - - /// - public Task RequiresExecutionAsync(InstallData model) - { - InstallState installState = GetInstallState(); - if (installState.HasFlag(InstallState.Unknown)) - { - // In this one case when it's a brand new install and nothing has been configured, make sure the - // back office cookie is cleared so there's no old cookies lying around causing problems - _cookieManager.ExpireCookie(_securitySettings.AuthCookieName); - } - - var shouldRun = installState.HasFlag(InstallState.Unknown) || !installState.HasFlag(InstallState.HasNonDefaultUser); - return Task.FromResult(shouldRun); - } - - private InstallState GetInstallState() - { - InstallState installState = InstallState.Unknown; - - if (_databaseBuilder.IsDatabaseConfigured) - { - installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; - } - - ConnectionStrings? umbracoConnectionString = _connectionStrings.CurrentValue; - - var isConnectionStringConfigured = umbracoConnectionString.IsConnectionStringConfigured(); - if (isConnectionStringConfigured) - { - installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; - } - - DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(umbracoConnectionString.ProviderName); - var isConnectionAvailable = isConnectionStringConfigured && DbConnectionExtensions.IsConnectionAvailable(umbracoConnectionString.ConnectionString, factory); - if (isConnectionAvailable) - { - installState = (installState | InstallState.CanConnect) & ~InstallState.Unknown; - } - - var isUmbracoInstalled = isConnectionAvailable && _databaseBuilder.IsUmbracoInstalled(); - if (isUmbracoInstalled) - { - installState = (installState | InstallState.UmbracoInstalled) & ~InstallState.Unknown; - } - - var hasSomeNonDefaultUser = isUmbracoInstalled && _databaseBuilder.HasSomeNonDefaultUser(); - if (hasSomeNonDefaultUser) - { - installState = (installState | InstallState.HasNonDefaultUser) & ~InstallState.Unknown; - } - - return installState; - } - - [Flags] - private enum InstallState : short - { - // This is an easy way to avoid 0 enum assignment and not worry about - // manual calcs. https://www.codeproject.com/Articles/396851/Ending-the-Great-Debate-on-Enum-Flags - Unknown = 1, - HasVersion = 1 << 1, - HasConnectionString = 1 << 2, - ConnectionStringConfigured = 1 << 3, - CanConnect = 1 << 4, - UmbracoInstalled = 1 << 5, - HasNonDefaultUser = 1 << 6 - } -} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs deleted file mode 100644 index dd78e149e5..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseConfigureStep.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Install; -using Umbraco.Cms.Core.Install.Models; -using Umbraco.Cms.Core.Mapping; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Extensions; -using Umbraco.New.Cms.Core.Installer; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; - -public class DatabaseConfigureStep : IInstallStep -{ - private readonly IOptionsMonitor _connectionStrings; - private readonly DatabaseBuilder _databaseBuilder; - private readonly ILogger _logger; - private readonly IUmbracoMapper _mapper; - - public DatabaseConfigureStep( - DatabaseBuilder databaseBuilder, - IOptionsMonitor connectionStrings, - ILogger logger, - IUmbracoMapper mapper) - { - _databaseBuilder = databaseBuilder; - _connectionStrings = connectionStrings; - _logger = logger; - _mapper = mapper; - } - - public Task ExecuteAsync(InstallData model) - { - DatabaseModel databaseModel = _mapper.Map(model.Database)!; - - if (!_databaseBuilder.ConfigureDatabaseConnection(databaseModel, false)) - { - throw new InstallException("Could not connect to the database"); - } - - return Task.CompletedTask; - } - - public Task RequiresExecutionAsync(InstallData model) - { - // If the connection string is already present in config we don't need to configure it again - if (_connectionStrings.CurrentValue.IsConnectionStringConfigured()) - { - try - { - // Since a connection string was present we verify the db can connect and query - _databaseBuilder.ValidateSchema(); - - return Task.FromResult(false); - } - catch (Exception ex) - { - // Something went wrong, could not connect so probably need to reconfigure - _logger.LogError(ex, "An error occurred, reconfiguring..."); - - return Task.FromResult(true); - } - } - - return Task.FromResult(true); - } -} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs deleted file mode 100644 index 9abe6823ab..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseInstallStep.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Install; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.New.Cms.Core.Installer; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; - -public class DatabaseInstallStep : IInstallStep, IUpgradeStep -{ - private readonly IRuntimeState _runtime; - private readonly DatabaseBuilder _databaseBuilder; - - public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) - { - _runtime = runtime; - _databaseBuilder = databaseBuilder; - } - - public Task ExecuteAsync(InstallData _) => Execute(); - - public Task ExecuteAsync() => Execute(); - - private Task Execute() - { - - if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) - { - _databaseBuilder.CreateDatabase(); - } - - DatabaseBuilder.Result? result = _databaseBuilder.CreateSchemaAndData(); - - if (result?.Success == false) - { - throw new InstallException("The database failed to install. ERROR: " + result.Message); - } - - return Task.CompletedTask; - } - - public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); - - public Task RequiresExecutionAsync() => ShouldExecute(); - - private Task ShouldExecute() - => Task.FromResult(true); -} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs deleted file mode 100644 index d0763f58dd..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/DatabaseUpgradeStep.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Install; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade; -using Umbraco.New.Cms.Core.Installer; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; - -public class DatabaseUpgradeStep : IInstallStep, IUpgradeStep -{ - private readonly DatabaseBuilder _databaseBuilder; - private readonly IRuntimeState _runtime; - private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IKeyValueService _keyValueService; - - public DatabaseUpgradeStep( - DatabaseBuilder databaseBuilder, - IRuntimeState runtime, - ILogger logger, - IUmbracoVersion umbracoVersion, - IKeyValueService keyValueService) - { - _databaseBuilder = databaseBuilder; - _runtime = runtime; - _logger = logger; - _umbracoVersion = umbracoVersion; - _keyValueService = keyValueService; - } - - public Task ExecuteAsync(InstallData _) => Execute(); - - public Task ExecuteAsync() => Execute(); - - private Task Execute() - { - _logger.LogInformation("Running 'Upgrade' service"); - - var plan = new UmbracoPlan(_umbracoVersion); - // TODO: Clear CSRF cookies with notification. - - DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); - - if (result?.Success == false) - { - throw new InstallException("The database failed to upgrade. ERROR: " + result.Message); - } - - return Task.CompletedTask; - } - - public Task RequiresExecutionAsync(InstallData model) => ShouldExecute(); - - public Task RequiresExecutionAsync() => ShouldExecute(); - - private Task ShouldExecute() - { - // Don't do anything if RunTimeLevel is not Install/Upgrade - if (_runtime.Level == RuntimeLevel.Run) - { - return Task.FromResult(false); - } - - // Check the upgrade state, if it matches we dont have to upgrade. - var plan = new UmbracoPlan(_umbracoVersion); - var currentState = _keyValueService.GetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name); - if (currentState != plan.FinalState) - { - return Task.FromResult(true); - } - - return Task.FromResult(false); - } -} diff --git a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs b/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs deleted file mode 100644 index 53989bf3b7..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Installer/Steps/RegisterInstallCompleteStep.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Umbraco.Cms.Infrastructure.Install; -using Umbraco.New.Cms.Core.Installer; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Infrastructure.Installer.Steps; - -public class RegisterInstallCompleteStep : IInstallStep, IUpgradeStep -{ - private readonly InstallHelper _installHelper; - - public RegisterInstallCompleteStep(InstallHelper installHelper) => _installHelper = installHelper; - - public Task ExecuteAsync(InstallData _) => Execute(); - - public Task ExecuteAsync() => Execute(); - - private Task Execute() => _installHelper.SetInstallStatusAsync(true, string.Empty); - - public Task RequiresExecutionAsync(InstallData _) => ShouldExecute(); - - public Task RequiresExecutionAsync() => ShouldExecute(); - - private static Task ShouldExecute() => Task.FromResult(true); -} diff --git a/src/Umbraco.New.Cms.Infrastructure/Security/IBackOfficeApplicationManager.cs b/src/Umbraco.New.Cms.Infrastructure/Security/IBackOfficeApplicationManager.cs deleted file mode 100644 index 068f5df472..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Security/IBackOfficeApplicationManager.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.New.Cms.Infrastructure.Security; - -public interface IBackOfficeApplicationManager -{ - Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default); -} diff --git a/src/Umbraco.New.Cms.Infrastructure/Services/IIndexingRebuilderService.cs b/src/Umbraco.New.Cms.Infrastructure/Services/IIndexingRebuilderService.cs deleted file mode 100644 index 7832a2b782..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Services/IIndexingRebuilderService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Examine; - -namespace Umbraco.New.Cms.Infrastructure.Services; - -public interface IIndexingRebuilderService -{ - bool CanRebuild(string indexName); - bool TryRebuild(IIndex index, string indexName); - - bool IsRebuilding(string indexName); -} diff --git a/src/Umbraco.New.Cms.Infrastructure/Services/IndexingRebuilderService.cs b/src/Umbraco.New.Cms.Infrastructure/Services/IndexingRebuilderService.cs deleted file mode 100644 index f6e9183d0a..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Services/IndexingRebuilderService.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Examine; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Infrastructure.Examine; - -namespace Umbraco.New.Cms.Infrastructure.Services; - -public class IndexingRebuilderService : IIndexingRebuilderService -{ - private const string TempKey = "temp_indexing_op_"; - private readonly IAppPolicyCache _runtimeCache; - private readonly IIndexRebuilder _indexRebuilder; - private readonly ILogger _logger; - - public IndexingRebuilderService( - AppCaches runtimeCache, - IIndexRebuilder indexRebuilder, - ILogger logger) - { - _indexRebuilder = indexRebuilder; - _logger = logger; - _runtimeCache = runtimeCache.RuntimeCache; - } - - public bool CanRebuild(string indexName) => _indexRebuilder.CanRebuild(indexName); - - public bool TryRebuild(IIndex index, string indexName) - { - // Remove it in case there's a handler there already - index.IndexOperationComplete -= Indexer_IndexOperationComplete; - - // Now add a single handler - index.IndexOperationComplete += Indexer_IndexOperationComplete; - - try - { - Set(indexName); - _indexRebuilder.RebuildIndex(indexName); - return true; - } - catch(Exception exception) - { - // Ensure it's not listening - index.IndexOperationComplete -= Indexer_IndexOperationComplete; - _logger.LogError(exception, "An error occurred rebuilding index"); - return false; - } - } - - private void Set(string indexName) - { - var cacheKey = TempKey + indexName; - - // put temp val in cache which is used as a rudimentary way to know when the indexing is done - _runtimeCache.Insert(cacheKey, () => "tempValue", TimeSpan.FromMinutes(5)); - } - - private void Clear(string? indexName) - { - var cacheKey = TempKey + indexName; - _runtimeCache.Clear(cacheKey); - } - - public bool IsRebuilding(string indexName) - { - var cacheKey = "temp_indexing_op_" + indexName; - return _runtimeCache.Get(cacheKey) is not null; - } - - private void Indexer_IndexOperationComplete(object? sender, EventArgs e) - { - var indexer = (IIndex?)sender; - - _logger.LogDebug("Logging operation completed for index {IndexName}", indexer?.Name); - - if (indexer is not null) - { - //ensure it's not listening anymore - indexer.IndexOperationComplete -= Indexer_IndexOperationComplete; - } - - _logger.LogInformation($"Rebuilding index '{indexer?.Name}' done."); - - Clear(indexer?.Name); - } -} 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 8f527c6851..0000000000 --- a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj +++ /dev/null @@ -1,17 +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/Installer/SignInUserStep.cs b/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs deleted file mode 100644 index accb5fd82b..0000000000 --- a/src/Umbraco.New.Cms.Web.Common/Installer/SignInUserStep.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Install.Models; -using Umbraco.Cms.Core.Security; -using Umbraco.Cms.Web.BackOffice.Security; -using Umbraco.New.Cms.Core.Installer; -using Umbraco.New.Cms.Core.Models.Installer; - -namespace Umbraco.New.Cms.Web.Common.Installer; - -public class SignInUserStep : IInstallStep -{ - private readonly IBackOfficeSignInManager _backOfficeSignInManager; - private readonly IBackOfficeUserManager _backOfficeUserManager; - - public SignInUserStep( - IBackOfficeSignInManager backOfficeSignInManager, - IBackOfficeUserManager backOfficeUserManager) - { - _backOfficeSignInManager = backOfficeSignInManager; - _backOfficeUserManager = backOfficeUserManager; - } - - public async Task ExecuteAsync(InstallData model) - { - BackOfficeIdentityUser? identityUser = await _backOfficeUserManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); - - if (identityUser is not null) - { - await _backOfficeSignInManager.SignInAsync(identityUser, false); - } - } - - public Task RequiresExecutionAsync(InstallData model) => Task.FromResult(true); -} 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 292532ba0c..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 - - - - - - - From f1a12e2a50f42dd1c557fdbebd0b979fe186b2c6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Oct 2023 13:13:32 +0100 Subject: [PATCH 04/20] Update docfx and use dotnet global tool --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 6cc18affbf..d506b7ed7c 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -149,7 +149,7 @@ stages: inputs: targetType: inline script: | - choco install docfx --version=2.59.4 -y + dotnet tool install -g docfx --version 2.72.1 if ($lastexitcode -ne 0){ throw ("Error installing DocFX") } From 73fd3c02fe6dd84444e46a88423bf512f9d794ff Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Oct 2023 13:55:26 +0100 Subject: [PATCH 05/20] use dotnet 8 for docs --- build/azure-pipelines.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index d506b7ed7c..7b543b201a 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -144,6 +144,11 @@ stages: pool: vmImage: 'windows-latest' steps: + - task: UseDotNet@2 + displayName: Use .NET $(dotnetVersion) + inputs: + version: $(dotnetVersion) + includePreviewVersions: $(dotnetIncludePreviewVersions) - task: PowerShell@2 displayName: Install DocFX inputs: From bab06b774963c7e5b5ad17668de63f2af45c2677 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:45:34 +0100 Subject: [PATCH 06/20] bump @umbraco-ui/uui from 1.4.0 to 1.5.0 --- src/Umbraco.Web.UI.Client/package-lock.json | 1099 ++++++++++--------- src/Umbraco.Web.UI.Client/package.json | 4 +- 2 files changed, 578 insertions(+), 525 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 171573e389..cf9238f1a8 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -7,8 +7,8 @@ "name": "ui", "dependencies": { "@microsoft/signalr": "7.0.12", - "@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", "ace-builds": "1.30.0", "angular": "1.8.3", "angular-animate": "1.8.3", @@ -2242,311 +2242,158 @@ "optional": true }, "node_modules/@types/trusted-types": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.4.tgz", - "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==" + "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.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==", @@ -2554,473 +2401,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/abab": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index bf2a4c9b0c..7b7f83c34b 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -19,8 +19,8 @@ }, "dependencies": { "@microsoft/signalr": "7.0.12", - "@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", "ace-builds": "1.30.0", "angular": "1.8.3", "angular-animate": "1.8.3", From 5bfd7d405f23b77916a7df48003bd25cb11d2f49 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 30 Oct 2023 14:22:24 +0100 Subject: [PATCH 07/20] bump @umbraco-ui/uui from 1.4.0 to 1.5.0 --- src/Umbraco.Web.UI.Login/package-lock.json | 1093 +++++++++-------- src/Umbraco.Web.UI.Login/package.json | 4 +- .../external-login-provider.element.ts | 2 +- 3 files changed, 576 insertions(+), 523 deletions(-) diff --git a/src/Umbraco.Web.UI.Login/package-lock.json b/src/Umbraco.Web.UI.Login/package-lock.json index ea0b8cd21e..ad36561339 100644 --- a/src/Umbraco.Web.UI.Login/package-lock.json +++ b/src/Umbraco.Web.UI.Login/package-lock.json @@ -6,8 +6,8 @@ "": { "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" @@ -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": { diff --git a/src/Umbraco.Web.UI.Login/package.json b/src/Umbraco.Web.UI.Login/package.json index 58a033f605..4e0d0d9c6b 100644 --- a/src/Umbraco.Web.UI.Login/package.json +++ b/src/Umbraco.Web.UI.Login/package.json @@ -13,8 +13,8 @@ "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" 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}
From 1b34d33eb7361c5b0f7888613398ebd25ab14fa9 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:06:14 +0100 Subject: [PATCH 08/20] Webhooks administration from Backoffice (#15050) * Create webhook models * Define interfaces for service and repository * Create Webhook dto and corresponding factory * implement WebhookRepository.cs * Remove entity name from models, as that should be resolved in mapping instead * Add new table to schema creator * Register repo for DI * Remove more mentions of entityname * Refactor repository to guids * Implement WebhookService * Use scopes in service * Start creating tests for service * Refactor delete to use Id and not entire entity * Rework Webhooks to be able to have multiple entity keys * Implement GetAll functionality * Implement webhook controller * Imeplement get all events action * Add equalityComparer deletegate to Webhook * Add datacontract attirbutes to properties * Implement backoffice webhooks tree * Implement first webhooks menu * Make WebHookController authorized * Update to have tabs with webhooks and logs * Enable create overlay * Push to entityKeys array * Fix up pagination * Implement delete functionality * remove pagination * add log view * Fix create to be able to select more than one content type * implement type name resolving for content * Refactor to use less duplication * Implement update functionality in frontend * Rename database table * Make multiple events possible * create new event picker * Refactor to actually add new database table with proper name * Make it possible to select multiple events * Fix updating current items * Fix up update functionality after db rework * Add webhook icon * Switch to match heartcore icons * Refactor to use bases instead of Enum * Refactor to make IWebhookEvent to Collection, so it can be injected instead of using reflection * Fix up frontend to match new models * Fix integration tests * Remove obsolete entity key from webhookdto * Introduce constants instead of hard coded strings * Start implementation of firing mechanism * Add new GetByEventName method * Add 1 to many list on WebhookDto * Implement new repository pattern * Implement GetByEventName * Fix up repository to use all async * Refactor events to fire * Refactor WebhookEvents to be more DRY * Add custom header * Start implementing log repository * Implement GetPaged * Implement WebhookLogService * Implement GetLogs * Add url to webhook log * Implement log overview * Formatting * Implement details view * Refactor to get actual retry count * Refactor firing to fire only when Enabled * Add Status code to detailed view * Add configuration to disable webhooks entirely * Implement custom headers frontend * Implement persistence of custom headers * Refactor retry service to also retry on non success status codes. * Refactor registration of Webhooks, to also register as NotificationHandler * Add webhooks migration * Add key for adding webhook headers * Fix up test * Change event icon to flag * Remember event, when editing what events you have chosen * Refactor reflection to check if INotificationAsyncHandler instead * Formatting * Refactor webhook model to no longer derive from EntityBase * Rename entityKeys to content keys * Rename controller to lowercase H * Add null check before trying to access selectedEvents * Add configuration for maximum number of retries * Add index to date * Add webhook Key to logs * Check for SchedulingPublisher before sending webhooks * rename requestObject to payload * Refactor event to send appropriate payloads * Refactor logging to happen for every try. * Order date by descending * Add todo * Change firing service to use String not ByteContent * Update Headers to Interface instead of concrete implementation * Dont return if a table exists already * Rename updateModel to webhook * Annotate WebhookController.cs with PluginController attribute * Add danish translations * Do not check if fail * Dont filter when selecting custom items * Remove delay from WebhookFiringService --------- Co-authored-by: Zeegaan --- .../Configuration/Models/WebhookSettings.cs | 34 ++++ src/Umbraco.Core/Constants-Applications.cs | 2 + src/Umbraco.Core/Constants-Configuration.cs | 1 + src/Umbraco.Core/Constants-Icons.cs | 5 + src/Umbraco.Core/Constants-WebhookEvents.cs | 32 +++ .../UmbracoBuilder.Collections.cs | 8 + .../UmbracoBuilder.Configuration.cs | 3 +- .../DependencyInjection/UmbracoBuilder.cs | 4 + .../EmbeddedResources/Lang/da.xml | 6 + .../EmbeddedResources/Lang/en_us.xml | 8 + .../Models/DefaultPayloadModel.cs | 6 + src/Umbraco.Core/Models/Webhook.cs | 27 +++ src/Umbraco.Core/Models/WebhookLog.cs | 28 +++ .../Models/WebhookResponseModel.cs | 8 + src/Umbraco.Core/PaginationHelper.cs | 15 ++ .../Persistence/Constants-DatabaseSchema.cs | 5 + .../Repositories/IWebhookLogRepository.cs | 11 ++ .../Repositories/IWebhookRepository.cs | 49 +++++ src/Umbraco.Core/Services/IWebHookService.cs | 18 ++ .../Services/IWebhookFiringService.cs | 8 + .../Services/IWebhookLogFactory.cs | 9 + .../Services/IWebhookLogService.cs | 11 ++ .../Services/WebhookLogFactory.cs | 31 +++ .../Services/WebhookLogService.cs | 33 ++++ src/Umbraco.Core/Services/WebhookService.cs | 85 ++++++++ .../Events/ContentDeleteWebhookEvent.cs | 30 +++ .../Events/ContentPublishWebhookEvent.cs | 48 +++++ .../Events/ContentUnpublishWebhookEvent.cs | 29 +++ .../Events/MediaDeleteWebhookEvent.cs | 29 +++ .../Webhooks/Events/MediaSaveWebhookEvent.cs | 48 +++++ src/Umbraco.Core/Webhooks/IWebhookEvent.cs | 6 + src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 73 +++++++ .../Webhooks/WebhookEventCollection.cs | 10 + .../Webhooks/WebhookEventCollectionBuilder.cs | 81 ++++++++ .../UmbracoBuilder.CoreServices.cs | 2 + .../UmbracoBuilder.Repositories.cs | 6 +- .../Install/DatabaseSchemaCreator.cs | 7 +- .../Migrations/Upgrade/UmbracoPlan.cs | 3 + .../Upgrade/V_13_0_0/AddWebhooks.cs | 41 ++++ .../Dtos/Webhook2ContentTypeKeysDto.cs | 20 ++ .../Persistence/Dtos/Webhook2EventsDto.cs | 18 ++ .../Persistence/Dtos/Webhook2HeadersDto.cs | 21 ++ .../Persistence/Dtos/WebhookDto.cs | 40 ++++ .../Persistence/Dtos/WebhookLogDto.cs | 59 ++++++ .../Persistence/Factories/WebhookFactory.cs | 58 ++++++ .../Factories/WebhookLogFactory.cs | 42 ++++ .../Implement/WebhookLogRepository.cs | 44 +++++ .../Implement/WebhookRepository.cs | 136 +++++++++++++ .../Implement/WebhookFiringService.cs | 74 +++++++ .../Controllers/BackOfficeServerVariables.cs | 4 + .../Controllers/WebhookController.cs | 90 +++++++++ .../UmbracoBuilderExtensions.cs | 4 +- .../Mapping/WebhookMapDefinition.cs | 58 ++++++ .../Trees/WebhooksTreeController.cs | 62 ++++++ .../Models/WebhookEventViewModel.cs | 10 + .../Models/WebhookLogViewModel.cs | 40 ++++ .../Models/WebhookViewModel.cs | 25 +++ .../src/common/resources/webhooks.resource.js | 47 +++++ .../src/common/services/editor.service.js | 9 +- .../eventpicker/eventpicker.controller.js | 102 ++++++++++ .../eventpicker/eventpicker.html | 43 +++++ .../src/views/webhooks/logs.controller.js | 39 ++++ .../src/views/webhooks/logs.html | 47 +++++ .../src/views/webhooks/overlays/details.html | 43 +++++ .../webhooks/overlays/edit.controller.js | 121 ++++++++++++ .../src/views/webhooks/overlays/edit.html | 141 ++++++++++++++ .../webhooks/overlays/header.controller.js | 21 ++ .../src/views/webhooks/overlays/header.html | 61 ++++++ .../src/views/webhooks/overview.controller.js | 64 ++++++ .../src/views/webhooks/overview.html | 23 +++ .../src/views/webhooks/webhooks.controller.js | 182 ++++++++++++++++++ .../src/views/webhooks/webhooks.html | 57 ++++++ .../Services/WebhookLogServiceTests.cs | 49 +++++ .../Services/WebhookServiceTests.cs | 106 ++++++++++ .../Umbraco.Core/Composing/TypeFinderTests.cs | 4 +- tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs | 2 + 76 files changed, 2818 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Core/Configuration/Models/WebhookSettings.cs create mode 100644 src/Umbraco.Core/Constants-WebhookEvents.cs create mode 100644 src/Umbraco.Core/Models/DefaultPayloadModel.cs create mode 100644 src/Umbraco.Core/Models/Webhook.cs create mode 100644 src/Umbraco.Core/Models/WebhookLog.cs create mode 100644 src/Umbraco.Core/Models/WebhookResponseModel.cs create mode 100644 src/Umbraco.Core/PaginationHelper.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/IWebhookLogRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs create mode 100644 src/Umbraco.Core/Services/IWebHookService.cs create mode 100644 src/Umbraco.Core/Services/IWebhookFiringService.cs create mode 100644 src/Umbraco.Core/Services/IWebhookLogFactory.cs create mode 100644 src/Umbraco.Core/Services/IWebhookLogService.cs create mode 100644 src/Umbraco.Core/Services/WebhookLogFactory.cs create mode 100644 src/Umbraco.Core/Services/WebhookLogService.cs create mode 100644 src/Umbraco.Core/Services/WebhookService.cs create mode 100644 src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/IWebhookEvent.cs create mode 100644 src/Umbraco.Core/Webhooks/WebhookEventBase.cs create mode 100644 src/Umbraco.Core/Webhooks/WebhookEventCollection.cs create mode 100644 src/Umbraco.Core/Webhooks/WebhookEventCollectionBuilder.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddWebhooks.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2ContentTypeKeysDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2EventsDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/Webhook2HeadersDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/WebhookDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Factories/WebhookFactory.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookLogRepository.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs create mode 100644 src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs create mode 100644 src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs create mode 100644 src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs create mode 100644 src/Umbraco.Web.BackOffice/Trees/WebhooksTreeController.cs create mode 100644 src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs create mode 100644 src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs create mode 100644 src/Umbraco.Web.Common/Models/WebhookViewModel.cs create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/webhooks.resource.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/eventpicker/eventpicker.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/logs.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/details.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/header.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overview.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overview.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs 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 4f9d045cb6..d29ee7019f 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -64,6 +64,7 @@ public static partial class Constants public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; + 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/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 73eb599695..e6b413b07f 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -19,6 +19,7 @@ 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; @@ -128,6 +129,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 +197,12 @@ public static partial class UmbracoBuilderExtensions public static SectionCollectionBuilder Sections(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 411c39b178..e09e731956 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -87,7 +87,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 098f770ebc..9e43c1eb35 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -40,6 +40,7 @@ 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 @@ -329,6 +330,9 @@ namespace Umbraco.Cms.Core.DependencyInjection // Register filestream security analyzers Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index c24bbdcdd0..73087fff30 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 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 3369f61af6..0cde55db08 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1975,6 +1975,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 +2126,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Settings Templating Third Party + Webhooks New update ready 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/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 new file mode 100644 index 0000000000..eb9049c1da --- /dev/null +++ b/src/Umbraco.Core/PaginationHelper.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core; + +public static class PaginationHelper +{ + public static void ConvertSkipTakeToPaging(int skip, int take, out long pageNumber, out int pageSize) + { + if (skip % take != 0) + { + throw new ArgumentException("Invalid skip/take, Skip must be a multiple of take - i.e. skip = 10, take = 5"); + } + + pageSize = take; + pageNumber = skip / take; + } +} diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 420f36c759..bfaad81c59 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -86,6 +86,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/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/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index d6531ca6e4..3e12664f03 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -38,6 +38,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; @@ -225,6 +226,7 @@ public static partial class UmbracoBuilderExtensions builder.AddPropertyIndexValueFactories(); builder.AddDeliveryApiCoreServices(); + builder.Services.AddTransient(); return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 3afb9fe64a..df2ac91839 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -66,8 +66,10 @@ 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/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index d422ea1445..9a944e4a90 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -82,7 +82,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 2844d53d80..306f7869f7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -94,5 +94,8 @@ 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}"); } } 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/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/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/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.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/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 994493e761..c973963495 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; @@ -91,6 +92,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/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/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/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/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/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/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/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..7716f83eda --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs @@ -0,0 +1,49 @@ +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, webHookLog.Date); + 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.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/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!; } } From 6658a521b29e8b9450e147b990afd2e2da0d5a67 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 31 Oct 2023 11:38:24 +0100 Subject: [PATCH 09/20] Dynamic Root (Alternative to XPath in MNTP) (#15035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Temp commit.. Initial work on XPath alternative for dymamically finding start nodes * First commit that goes all the way from ui to db for NearestAncestorOrSelf * Added more filters + return null from controller instead of not found * Bugfix * rewrite query to make sqlserver happy? * Added more tests * clean up initial step * Added tests and refactor * Update endpoint to take model instead of json * pick origin * Use model for config instead of string * append add filter button * fix * default filter * rename json fields * correct field names * minor corrections * Renaming.. * Rename endpoint * initial work for appending query steps * query steps ui * more localization * query step UI * Use doc type keys instead of alias * only for Documents * change to send keys to anyOfDocTypeKeys * Fix potential bug * Fix when level is impossible to get * correct prop to dynamicRoot * noValidStartNode dialog * custom query step * Renaming * Rollback unintended file change * Fixed issue if no doc type is chosen * Remove unintended file changes * More unintended changes * Renaming * Optimizations - IDE Recommendation for better source - Renaming for better clarity - Improving spacing/formatting - Typo corrections - Remove warnings concerning IEnumerable * Fix failed attempt bug --------- Co-authored-by: Niels Lyngsø Co-authored-by: Sven Geusens --- .../Services/SqliteSyntaxProvider.cs | 3 + .../UmbracoBuilder.Collections.cs | 22 + .../DependencyInjection/UmbracoBuilder.cs | 2 + .../DynamicRoot/DynamicRootContext.cs | 8 + .../DynamicRoot/DynamicRootNodeQuery.cs | 17 + .../DynamicRoot/DynamicRootService.cs | 74 ++ .../DynamicRoot/IDynamicRootService.cs | 9 + .../Origin/ByKeyDynamicRootOriginFinder.cs | 38 + .../Origin/CurrentDynamicRootOriginFinder.cs | 21 + .../DynamicRootOriginCollectionBuilder.cs | 8 + .../DynamicRootOriginFinderCollection.cs | 11 + .../Origin/IDynamicRootOriginFinder.cs | 9 + .../Origin/ParentDynamicRootOriginFinder.cs | 20 + .../Origin/RootDynamicRootOriginFinder.cs | 70 ++ .../Origin/SiteDynamicRootOriginFinder.cs | 55 ++ .../QuerySteps/DynamicRootQueryStep.cs | 11 + .../DynamicRootQueryStepCollection.cs | 11 + .../DynamicRootQueryStepCollectionBuilder.cs | 8 + ...thestAncestorOrSelfDynamicRootQueryStep.cs | 37 + ...estDescendantOrSelfDynamicRootQueryStep.cs | 35 + .../QuerySteps/IDynamicRootQueryStep.cs | 8 + .../QuerySteps/IDynamicRootRepository.cs | 12 + ...arestAncestorOrSelfDynamicRootQueryStep.cs | 37 + ...estDescendantOrSelfDynamicRootQueryStep.cs | 35 + .../EmbeddedResources/Lang/da.xml | 35 + .../EmbeddedResources/Lang/en.xml | 35 + .../EmbeddedResources/Lang/en_us.xml | 37 + .../Extensions/CollectionExtensions.cs | 8 + .../Extensions/EnumerableExtensions.cs | 1 + .../MultiNodePickerConfigurationTreeSource.cs | 27 + .../UmbracoBuilder.Repositories.cs | 3 + .../Logging/Serilog/LoggerConfigExtensions.cs | 2 +- .../Implement/DynamicRootRepository.cs | 125 +++ .../SqlSyntax/ISqlSyntaxProvider.cs | 4 + .../SqlSyntax/SqlSyntaxProviderBase.cs | 3 + .../Controllers/EntityController.cs | 107 ++- .../common/filters/nestedcontent.filter.js | 3 + .../src/common/resources/entity.resource.js | 14 + .../src/less/components/umb-node-preview.less | 7 +- .../pickdynamicrootcustomstep.controller.js | 40 + .../pickdynamicrootcustomstep.html | 55 ++ .../pickdynamicrootorigin.controller.js | 82 ++ .../pickdynamicrootorigin.html | 58 ++ .../pickdynamicrootquerystep.controller.js | 91 +++ .../pickdynamicrootquerystep.html | 58 ++ .../prevalueeditors/treesource.controller.js | 236 ++++-- .../src/views/prevalueeditors/treesource.html | 117 ++- .../contentpicker/contentpicker.controller.js | 44 +- .../Services/DynamicRootServiceTests.cs | 719 ++++++++++++++++++ 49 files changed, 2387 insertions(+), 85 deletions(-) create mode 100644 src/Umbraco.Core/DynamicRoot/DynamicRootContext.cs create mode 100644 src/Umbraco.Core/DynamicRoot/DynamicRootNodeQuery.cs create mode 100644 src/Umbraco.Core/DynamicRoot/DynamicRootService.cs create mode 100644 src/Umbraco.Core/DynamicRoot/IDynamicRootService.cs create mode 100644 src/Umbraco.Core/DynamicRoot/Origin/ByKeyDynamicRootOriginFinder.cs create mode 100644 src/Umbraco.Core/DynamicRoot/Origin/CurrentDynamicRootOriginFinder.cs create mode 100644 src/Umbraco.Core/DynamicRoot/Origin/DynamicRootOriginCollectionBuilder.cs create mode 100644 src/Umbraco.Core/DynamicRoot/Origin/DynamicRootOriginFinderCollection.cs create mode 100644 src/Umbraco.Core/DynamicRoot/Origin/IDynamicRootOriginFinder.cs create mode 100644 src/Umbraco.Core/DynamicRoot/Origin/ParentDynamicRootOriginFinder.cs create mode 100644 src/Umbraco.Core/DynamicRoot/Origin/RootDynamicRootOriginFinder.cs create mode 100644 src/Umbraco.Core/DynamicRoot/Origin/SiteDynamicRootOriginFinder.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStep.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStepCollection.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/DynamicRootQueryStepCollectionBuilder.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/FarthestAncestorOrSelfDynamicRootQueryStep.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/FarthestDescendantOrSelfDynamicRootQueryStep.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/IDynamicRootQueryStep.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/IDynamicRootRepository.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/NearestAncestorOrSelfDynamicRootQueryStep.cs create mode 100644 src/Umbraco.Core/DynamicRoot/QuerySteps/NearestDescendantOrSelfDynamicRootQueryStep.cs create mode 100644 src/Umbraco.Core/Extensions/CollectionExtensions.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DynamicRootRepository.cs create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootcustomstep/pickdynamicrootcustomstep.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootorigin/pickdynamicrootorigin.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/pickdynamicrootquerystep/pickdynamicrootquerystep.html create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DynamicRootServiceTests.cs diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs index af9147b29a..4082b828e9 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -468,4 +468,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/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index e6b413b07f..a34d4e377c 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -15,6 +15,8 @@ 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; @@ -78,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() @@ -197,6 +213,12 @@ 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. /// diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 9e43c1eb35..0e132d3fed 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -36,6 +36,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; 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; @@ -330,6 +331,7 @@ 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/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 73087fff30..a4217a3525 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -1221,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 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 8c35554043..6047de2b10 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 diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 0cde55db08..c55b78a38b 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 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/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.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index df2ac91839..2e4832cff0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Persistence.Repositories; +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.Extensions; @@ -68,6 +70,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); 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/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/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.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.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/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/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/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/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()); + } +} From 8e7831f4b85586cdf85bdc3294c8a1cad8af77e3 Mon Sep 17 00:00:00 2001 From: Jeffrey Schoemaker Date: Tue, 31 Oct 2023 12:17:34 +0100 Subject: [PATCH 10/20] Update NestedContentPropertyEditor.cs - Update obsolete message (#14953) Nested Content is removed in v14, not in v13 anymore. This makes sure developers won't get confused / in panic mode --- .../PropertyEditors/NestedContentPropertyEditor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index bf5781079a..1bfa3a7ed0 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, From 767770891723d483c84b6f8d6de01b0adaca1245 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 31 Oct 2023 12:18:51 +0100 Subject: [PATCH 11/20] Fix `JsonNetSerializer` settings leaking into derived implementations (#14814) * Change settings static field to instance property * Rename property to Settings --- .../ConfigurationEditorJsonSerializer.cs | 6 ++---- .../Serialization/JsonNetSerializer.cs | 11 ++++------- 2 files changed, 6 insertions(+), 11 deletions(-) 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); From eb355f4ab68a165d1df7f3d0f7e98245aa024b73 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Tue, 31 Oct 2023 12:30:12 +0100 Subject: [PATCH 12/20] Compare invariant to string instead, as SqlServer rounds DateTime --- .../Umbraco.Core/Services/WebhookLogServiceTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs index 7716f83eda..af75becd1c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using System.Globalization; +using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; @@ -37,7 +38,7 @@ public class WebhookLogServiceTests : UmbracoIntegrationTest Assert.IsNotEmpty(webhookLogsPaged.Items); Assert.AreEqual(1, webhookLogsPaged.Items.Count()); var webHookLog = webhookLogsPaged.Items.First(); - Assert.AreEqual(createdWebhookLog.Date, webHookLog.Date); + 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); From 53b87cb78ce3b3c3cd19843080db1a25ece94df9 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:34:52 +0100 Subject: [PATCH 13/20] V13: npm updates (ace-editor) (#15033) * bump vite from 4.4.11 to 4.5.0 * bump ace editor from 1.30.0 to 1.31.0 * bump eslint from 8.51.0 to 8.52.0 * update lockfile --- src/Umbraco.Web.UI.Client/package-lock.json | 203 ++++++++++++-------- src/Umbraco.Web.UI.Client/package.json | 4 +- src/Umbraco.Web.UI.Login/package-lock.json | 8 +- src/Umbraco.Web.UI.Login/package.json | 2 +- 4 files changed, 126 insertions(+), 91 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index aa65c3e335..ddcc167622 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -7,7 +7,7 @@ "name": "ui", "dependencies": { "@microsoft/signalr": "7.0.12", - "ace-builds": "1.30.0", + "ace-builds": "1.31.0", "angular": "1.8.3", "angular-animate": "1.8.3", "angular-aria": "1.8.3", @@ -47,7 +47,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 +296,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 +1875,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 +1991,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 +2018,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": { @@ -2170,10 +2170,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 +2229,12 @@ "dev": true, "optional": true }, + "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 +2266,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 +2349,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 +2358,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 +3665,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": { @@ -5528,9 +5537,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 +5550,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 +5806,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 +6795,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 +7365,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 +7536,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 +7549,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 +7579,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 +7601,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 +7799,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 +8526,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 +8596,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 +8727,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 +8812,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 +8870,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", @@ -11231,9 +11241,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 +11912,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 +12600,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 +14465,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 +14786,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 +15909,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..5d12153e62 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@microsoft/signalr": "7.0.12", - "ace-builds": "1.30.0", + "ace-builds": "1.31.0", "angular": "1.8.3", "angular-animate": "1.8.3", "angular-aria": "1.8.3", @@ -59,7 +59,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.Login/package-lock.json b/src/Umbraco.Web.UI.Login/package-lock.json index ad36561339..d7c296dd28 100644 --- a/src/Umbraco.Web.UI.Login/package-lock.json +++ b/src/Umbraco.Web.UI.Login/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "typescript": "^5.2.2", - "vite": "^4.4.11" + "vite": "^4.5.0" }, "engines": { "node": ">=20.8", @@ -2555,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 4e0d0d9c6b..57c7793b75 100644 --- a/src/Umbraco.Web.UI.Login/package.json +++ b/src/Umbraco.Web.UI.Login/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "typescript": "^5.2.2", - "vite": "^4.4.11" + "vite": "^4.5.0" }, "msw": { "workerDirectory": "public" From d08d141bcf5eb16a5add5655a80f34ac93ac48fb Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 31 Oct 2023 12:38:44 +0100 Subject: [PATCH 14/20] Updates and support for re-use of CMS logic in Deploy (#14990) * Adds additional parameter to IFileSource.GetFilesAsync allowing the caller to continue on a file not found exception. * Moved redirect tracking and creation logic out of handler into a service, allowing for re-use in Deploy. * Reverted breaking change in IFileSource by obsoleting old method and --- src/Umbraco.Core/Deploy/IFileSource.cs | 18 +- src/Umbraco.Core/Routing/IRedirectTracker.cs | 23 +++ .../UmbracoBuilder.CoreServices.cs | 4 + .../Routing/RedirectTracker.cs | 125 +++++++++++++ .../Routing/RedirectTrackingHandler.cs | 173 ++---------------- 5 files changed, 179 insertions(+), 164 deletions(-) create mode 100644 src/Umbraco.Core/Routing/IRedirectTracker.cs create mode 100644 src/Umbraco.Infrastructure/Routing/RedirectTracker.cs 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/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.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 3e12664f03..1c21352af2 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -52,6 +52,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; @@ -216,6 +217,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) 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]!; } } From ae84d324ab873650ab5f9e4ef334453a27b149dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 31 Oct 2023 12:52:35 +0100 Subject: [PATCH 15/20] V13/feature/blocks in rte (#15029) * insert umb rte block web component in rte * First stab at moving the RTE markup to a nested "markup" property in the property value. * initial work * only rewrite markup * transform RTE into component * parse scope in grid.rte * revert use a fallback instead * block insertion and sync in place * block picker partly impl * remove test of old controller * remove test of old controller * block with block data * proper block with api connection * remove log * styling * Persist blocks data (still a temporary solution) * styling allows for interaction * block actions * tinyMCE styling * paste feature * prevalue display Inline toggle * inline mode in RTE * todo note * fixes wording * preparation for editor communication * remove val-server-match for now * clean up blocks that does not belong in markup * remove blocks not used in the markup * liveEditing * displayAsBlock formatting * clean up * TODO note * Serverside handling for RTE blocks (incl. refactor of Block List and Block Grid) * ensure rich text loads after block editor * trigger resize on block init * Handle RTE blocks output in Delivery API * sanitize ng classes * simplify calls to init blocks * move sanitisation * make validation work * only warn when missing one * clean up * remove validation border as it does not work * more clean up * add unsupported block entry editor * Revert breaking functionality for Block List and Grid * prevent re-inits of blocks * make sure delete blocks triggers an update * Refactor RichTextPropertyIndexValueFactory to index values from blocks + clean up RichTextPropertyEditor dependencies * first working cursor solution * inline element approach * Handle both inline and block level blocks * Fix the RTE block parser regex so it handles multiple inline blocks. * Fix reference and tags tracking, add tests, make the editor backwards compatible and make deploy happy * Use RichTextPropertyEditorHelper serialization in tests * Ensure correct model in Block Grid value converter (incl unit test to prove it) * do not include umbblockpicker in grid * make blocks the new default, instead of macros * only send value of body from DOMParser * Blocks of deleted ElementTypes shows unsupported * do not edit a unsupported block * remove trying to be smart on the init * fix missing culture issue * set dirty * alert when no blocks * Revert "make blocks the new default, instead of macros" This reverts commit 283e8aa473fdfde075197d34aa47e35dfc64a8ae. --------- Co-authored-by: kjac --- .../Json/DeliveryApiJsonTypeResolver.cs | 2 +- .../Blocks/IPartialViewBlockEngine.cs | 9 + .../Models/RichTextEditorSettings.cs | 4 + .../DeliveryApi/IApiRichTextElementParser.cs | 7 +- .../EmbeddedResources/Lang/da.xml | 2 + .../EmbeddedResources/Lang/en.xml | 2 + .../EmbeddedResources/Lang/en_us.xml | 2 + .../Models/Blocks/RichTextBlockItem.cs | 129 +++ .../Models/Blocks/RichTextBlockModel.cs | 38 + .../Models/DeliveryApi/RichTextModel.cs | 4 + .../Models/DeliveryApi/RichTextRootElement.cs | 19 + .../IRichTextPropertyIndexValueFactory.cs | 5 + .../PropertyEditors/RichTextConfiguration.cs | 47 +- .../DeliveryApi/ApiRichTextElementParser.cs | 87 +- .../DeliveryApi/ApiRichTextMarkupParser.cs | 23 + .../UmbracoBuilder.CoreServices.cs | 1 + .../DeliveryApiBlockReferenceExtensions.cs | 16 + .../Models/Blocks/BlockEditorDataConverter.cs | 18 +- .../Models/Blocks/RichTextBlockLayoutItem.cs | 21 + .../RichTextEditorBlockDataConverter.cs | 20 + .../Models/RichTextEditorValue.cs | 14 + .../BlockEditorPropertyValueEditor.cs | 151 +-- .../PropertyEditors/BlockEditorValidator.cs | 50 +- .../BlockEditorValidatorBase.cs | 51 + .../PropertyEditors/BlockEditorValues.cs | 10 + .../BlockValuePropertyValueEditorBase.cs | 185 ++++ .../RichTextEditorBlockValidator.cs | 40 + .../PropertyEditors/RichTextPropertyEditor.cs | 250 +++-- .../RichTextPropertyEditorHelper.cs | 62 ++ .../RichTextPropertyIndexValueFactory.cs | 68 ++ .../BlockGridPropertyValueConverter.cs | 82 +- .../BlockGridPropertyValueCreator.cs | 68 ++ .../BlockListPropertyValueConverter.cs | 49 +- .../BlockListPropertyValueCreator.cs | 35 + .../BlockPropertyValueConverterBase.cs | 1 + .../BlockPropertyValueCreatorBase.cs | 266 +++++ .../RichTextBlockPropertyValueCreator.cs | 38 + .../ValueConverters/RichTextParsingRegexes.cs | 9 + .../RteMacroRenderingValueConverter.cs | 131 ++- .../Blocks/PartialViewBlockEngine.cs | 74 ++ .../UmbracoBuilderExtensions.cs | 3 + src/Umbraco.Web.UI.Client/gulp/config.js | 3 +- .../components/grid/grid.rte.directive.js | 8 +- .../validation/valservermatch.directive.js | 22 +- .../blockeditormodelobject.service.js | 47 +- .../src/common/services/tinymce.service.js | 198 +++- .../src/less/rte-content.less | 8 + src/Umbraco.Web.UI.Client/src/less/rte.less | 11 + .../umbBlockListPropertyEditor.component.js | 24 +- .../blocklist/umbblocklistblock.component.js | 2 +- .../views/propertyeditors/rte/blockrteui.less | 114 +++ .../labelblock/rtelabelblock.editor.html | 58 ++ .../unsupportedblock.editor.html | 59 ++ .../blockrte.blockconfiguration.controller.js | 246 +++++ .../prevalue/blockrte.blockconfiguration.html | 25 + ...e.blockconfiguration.overlay.controller.js | 314 ++++++ .../blockrte.blockconfiguration.overlay.html | 281 ++++++ .../blockrte.blockconfiguration.overlay.less | 103 ++ .../rte/blocks/umb-rte-block.component.js | 127 +++ .../propertyeditors/rte/rte.component.js | 955 ++++++++++++++++++ .../propertyeditors/rte/rte.controller.js | 133 --- .../src/views/propertyeditors/rte/rte.html | 14 +- .../rte/rte.prevalues.controller.js | 10 +- .../rte/umb-rte-property-editor.html | 10 + .../propertyeditors/rte-controller.spec.js | 11 +- .../RichTextPropertyEditorTests.cs | 179 ++++ .../DeliveryApi/RichTextParserTests.cs | 190 +++- .../BlockGridPropertyValueConverterTests.cs | 48 + .../BlockListPropertyValueConverterTests.cs | 104 +- .../BlockPropertyValueConverterTestsBase.cs | 64 ++ .../RichTextPropertyEditorHelperTests.cs | 178 ++++ 71 files changed, 4945 insertions(+), 694 deletions(-) create mode 100644 src/Umbraco.Core/Blocks/IPartialViewBlockEngine.cs create mode 100644 src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs create mode 100644 src/Umbraco.Core/Models/Blocks/RichTextBlockModel.cs create mode 100644 src/Umbraco.Core/Models/DeliveryApi/RichTextRootElement.cs create mode 100644 src/Umbraco.Core/PropertyEditors/IRichTextPropertyIndexValueFactory.cs create mode 100644 src/Umbraco.Infrastructure/Extensions/DeliveryApiBlockReferenceExtensions.cs create mode 100644 src/Umbraco.Infrastructure/Models/Blocks/RichTextBlockLayoutItem.cs create mode 100644 src/Umbraco.Infrastructure/Models/Blocks/RichTextEditorBlockDataConverter.cs create mode 100644 src/Umbraco.Infrastructure/Models/RichTextEditorValue.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorHelper.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs create mode 100644 src/Umbraco.Web.Common/Blocks/PartialViewBlockEngine.cs create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blockrteui.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/blockrteentryeditors/labelblock/rtelabelblock.editor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/blockrteentryeditors/unsupportedblock/unsupportedblock.editor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/prevalue/blockrte.blockconfiguration.overlay.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/blocks/umb-rte-block.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/umb-rte-property-editor.html create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs 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.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/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/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index a4217a3525..128ed70229 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -2320,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 6047de2b10..9d1b110b8a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -2883,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 c55b78a38b..77272aa79b 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -2997,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/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/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/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/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/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.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 1c21352af2..373c4a2008 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -240,6 +240,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/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/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs index 2f2c7ae1ec..350ce31ab2 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs @@ -43,15 +43,7 @@ public abstract class BlockEditorDataConverter return Convert(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); - - private BlockEditorData Convert(BlockValue? value) + public BlockEditorData Convert(BlockValue? value) { if (value?.Layout == null) { @@ -65,4 +57,12 @@ public abstract class BlockEditorDataConverter 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/Blocks/RichTextBlockLayoutItem.cs b/src/Umbraco.Infrastructure/Models/Blocks/RichTextBlockLayoutItem.cs new file mode 100644 index 0000000000..93179e82ed --- /dev/null +++ b/src/Umbraco.Infrastructure/Models/Blocks/RichTextBlockLayoutItem.cs @@ -0,0 +1,21 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Newtonsoft.Json; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Used for deserializing the rich text block layouts +/// +public class RichTextBlockLayoutItem : IBlockLayoutItem +{ + [JsonProperty("contentUdi", Required = Required.Always)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? ContentUdi { get; set; } + + [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? SettingsUdi { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Models/Blocks/RichTextEditorBlockDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/RichTextEditorBlockDataConverter.cs new file mode 100644 index 0000000000..e47fd82a52 --- /dev/null +++ b/src/Umbraco.Infrastructure/Models/Blocks/RichTextEditorBlockDataConverter.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json.Linq; + +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.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/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index c524c2c39b..b8d9a6c468 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -13,12 +13,9 @@ using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Core.PropertyEditors; -internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags +internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValueEditorBase { private BlockEditorValues? _blockEditorValues; - private readonly IDataTypeService _dataTypeService; - private readonly ILogger _logger; - private readonly PropertyEditorCollection _propertyEditors; protected BlockEditorPropertyValueEditor( DataEditorAttribute attribute, @@ -29,11 +26,8 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV 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; } protected BlockEditorValues BlockEditorValues @@ -42,7 +36,8 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV 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(); @@ -53,32 +48,11 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV 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(); @@ -88,31 +62,9 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV 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? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration; - - result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); - } - } - - return result; + return GetBlockValueTags(blockEditorData.BlockValue, languageId); } - #region Convert database // editor - // note: there is NO variant support here /// @@ -142,8 +94,7 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV return string.Empty; } - MapBlockItemDataToEditor(property, blockEditorData.BlockValue.ContentData); - MapBlockItemDataToEditor(property, blockEditorData.BlockValue.SettingsData); + MapBlockValueToEditor(property, blockEditorData.BlockValue); // return json convertable object return blockEditorData.BlockValue; @@ -178,93 +129,9 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV return string.Empty; } - MapBlockItemDataFromEditor(blockEditorData.BlockValue.ContentData); - MapBlockItemDataFromEditor(blockEditorData.BlockValue.SettingsData); + MapBlockValueFromEditor(blockEditorData.BlockValue); // return json return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } - - 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; - } - } - } - - #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs index 0e4e99f421..8e17c6c477 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs @@ -1,65 +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 +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 773b9e3a62..3270351838 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs @@ -34,7 +34,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/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 053e98d9cb..8d38b218b5 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,65 @@ 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( - editorValue.Value.ToString()!, mediaParentId, userId); + richTextEditorValue.Markup, mediaParentId, userId); var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(parseAndSaveBase64Images, mediaParentId, userId); 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 d7748f7e98..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,65 +103,28 @@ 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 - var configuration = propertyType.DataType.ConfigurationAs(); + BlockGridConfiguration? configuration = propertyType.DataType.ConfigurationAs(); if (configuration is null) { 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 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/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 52efe3755a..4c65963093 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Logging; @@ -11,17 +10,18 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; 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; [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V14")] @@ -37,9 +37,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) @@ -143,26 +150,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 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/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 7da2bd8b7a..d10412dd4a 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 : IBlockLayoutItem 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.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 74977c9969..2dd828f9c2 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; @@ -46,6 +47,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; @@ -339,6 +341,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.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/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/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/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/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/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/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/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/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/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 9e84217ab7..f0971ffee6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -5,60 +5,19 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Cms.Core.Models.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.Core.PublishedCache; using Umbraco.Cms.Core.Services; 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() { @@ -78,40 +37,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( @@ -136,7 +86,7 @@ public class BlockListPropertyValueConverterTests } [Test] - public void Get_Value_Type_Single() + public void Get_Value_TypeSingle() { var editor = CreateConverter(); var config = ConfigForSingle(); @@ -151,7 +101,7 @@ public class BlockListPropertyValueConverterTests } [Test] - public void Get_Value_Type_SingleBlockMode() + public void Get_Value_TypeSingleBlockMode() { var editor = CreateConverter(); var config = ConfigForSingleBlockMode(); @@ -263,7 +213,7 @@ data: []}"; }, contentData: [ { - 'contentTypeKey': '" + _contentKey1 + @"', + 'contentTypeKey': '" + ContentKey1 + @"', 'key': '1304E1DD-0000-4396-84FE-8A399231CB3D' } ] @@ -294,7 +244,7 @@ data: []}"; }, contentData: [ { - 'contentTypeKey': '" + _contentKey1 + @"', + 'contentTypeKey': '" + ContentKey1 + @"', 'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D' } ] @@ -336,29 +286,29 @@ data: []}"; }, 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' } ], @@ -385,7 +335,7 @@ data: []}"; } [Test] - public void Data_Item_Removed_If_Removed_From_Config() + public void Data_Item_Removed_If_Removed_FromConfig() { var editor = CreateConverter(); @@ -397,7 +347,7 @@ data: []}"; { new BlockListConfiguration.BlockConfiguration { - ContentElementTypeKey = _contentKey2, + ContentElementTypeKey = ContentKey2, SettingsElementTypeKey = null, }, }, @@ -422,29 +372,29 @@ data: []}"; }, 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(); +} From 78f46abe63025094a51314ab546ee74d397a7f27 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:10:27 +0100 Subject: [PATCH 16/20] V13/bugfix/login screen autofill (#15075) * pass in fields from outside login-page to make autofill APIs work * remove unused property * disable spellcheck for login form * add name to login form * remove unused property * clean-up imports * use inputmode instead of type=email * refactor arguments into opts object --- src/Umbraco.Web.UI.Login/src/auth-styles.css | 34 ++ src/Umbraco.Web.UI.Login/src/auth.element.ts | 308 ++++++++++++----- .../src/components/login-input.element.ts | 18 + .../components/pages/login.page.element.ts | 313 ++++++++---------- src/Umbraco.Web.UI.Login/src/index.ts | 2 + 5 files changed, 412 insertions(+), 263 deletions(-) create mode 100644 src/Umbraco.Web.UI.Login/src/auth-styles.css create mode 100644 src/Umbraco.Web.UI.Login/src/components/login-input.element.ts 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/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'; From 15fb4205ff01ccb46627066251e1b92c1d914fe0 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 31 Oct 2023 17:14:25 +0100 Subject: [PATCH 17/20] Fixed issue with member creation and fixed test with non-breakable-space issue --- .../src/common/services/umbdataformatter.service.js | 2 ++ .../tests/DefaultConfig/Content/content.spec.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) 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/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts index 5ece8117b3..4a10071326 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts @@ -623,7 +623,7 @@ test.describe('Content tests', () => { await umbracoUi.isSuccessNotificationVisible(); // Ensure that the view gets rendered correctly - const expected = `

     

    Acceptance test

     

    `; + const expected = `

     

    Acceptancetest

     

    `; await expect(await umbracoApi.content.verifyRenderedContent('/', expected, true)).toBeTruthy(); // Cleanup @@ -737,4 +737,3 @@ test.describe('Content tests', () => { await umbracoApi.partialViews.ensureMacroFileNameNotExists(macroFileName); }); }); - From 890da5f57bd605a907aa161a4d2808c2a61fcf56 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 31 Oct 2023 18:45:09 +0100 Subject: [PATCH 18/20] Fixed login tests --- .../tests/DefaultConfig/Login/login.spec.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Login/login.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Login/login.spec.ts index 5c8d376eed..64dfb64dca 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Login/login.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Login/login.spec.ts @@ -18,8 +18,8 @@ test.describe('Login', () => { await expect(error).toBeHidden(); // Action - await page.fill('#umb-username input', process.env.UMBRACO_USER_LOGIN); - await page.fill('#umb-password input', process.env.UMBRACO_USER_PASSWORD); + await page.fill('#username-input input', process.env.UMBRACO_USER_LOGIN); + await page.fill('#password-input input', process.env.UMBRACO_USER_PASSWORD); await page.locator('#umb-login-button').click(); await page.waitForURL(process.env.URL + '/umbraco#/content'); @@ -32,18 +32,18 @@ test.describe('Login', () => { const username = process.env.UMBRACO_USER_LOGIN; const password = 'wrong'; - // Precondition + // Precondition« let error = page.locator('.text-error'); await expect(error).toBeHidden(); // Action - await page.fill('#umb-username input', username); - await page.fill('#umb-password input', password); + await page.fill('#username-input input', username); + await page.fill('#password-input input', password); await page.locator('#umb-login-button').click(); // Assert - let usernameField = await page.locator('#umb-username'); - let passwordField = await page.locator('#umb-password'); + let usernameField = await page.locator('#username-input'); + let passwordField = await page.locator('#password-input'); await expect(error).toBeVisible(); await expect(usernameField).toBeVisible(); await expect(passwordField).toBeVisible(); @@ -58,13 +58,13 @@ test.describe('Login', () => { await expect(error).toBeHidden(); // Action - await page.fill('#umb-username input', username); - await page.fill('#umb-password input', password); + await page.fill('#username-input input', username); + await page.fill('#password-input input', password); await page.locator('#umb-login-button').click(); // Assert - let usernameField = await page.locator('#umb-username'); - let passwordField = await page.locator('#umb-password'); + let usernameField = await page.locator('#username-input'); + let passwordField = await page.locator('#password-input'); await expect(error).toBeVisible(); await expect(usernameField).toBeVisible(); await expect(passwordField).toBeVisible(); From aaf7171f7556b57450404b6f706c34d288df4b52 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 31 Oct 2023 19:36:43 +0100 Subject: [PATCH 19/20] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index e4ab52430e..01fadfd808 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.0.0-rc1", + "version": "13.0.0-rc2", "assemblyVersion": { "precision": "build" }, From 92af8ac881638e8a29f7dfc64ef0799a950a3842 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:30:32 +0100 Subject: [PATCH 20/20] Check content permissions before performing action (#15043) * Setting actionContext.Result when authz wasn't successful * Taking into account permissions when it is a new node * Cleanup * Passing nodeId as path when new item --- .../Security/ContentPermissions.cs | 59 ++++++++----------- .../Controllers/ContentController.cs | 23 ++++---- .../Controllers/ContentControllerBase.cs | 1 - .../Filters/ContentSaveValidationAttribute.cs | 1 + 4 files changed, 36 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Core/Security/ContentPermissions.cs b/src/Umbraco.Core/Security/ContentPermissions.cs index db27d100c6..0ad85971cf 100644 --- a/src/Umbraco.Core/Security/ContentPermissions.cs +++ b/src/Umbraco.Core/Security/ContentPermissions.cs @@ -167,12 +167,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) @@ -183,19 +178,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) { @@ -208,7 +201,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; } @@ -235,12 +229,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) @@ -251,19 +240,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) { @@ -276,7 +263,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; } @@ -288,8 +276,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.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 944068114a..9ec67319d9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -759,6 +759,7 @@ public class ContentController : ContentControllerBase return pagedResult; } + /// /// Creates a blueprint from a content item /// @@ -1057,7 +1058,7 @@ public class ContentController : ContentControllerBase AddDomainWarnings(publishStatus.Content, successfulCultures, globalNotifications); AddPublishStatusNotifications(new[] { publishStatus }, globalNotifications, notifications, successfulCultures); } - break; + break; case ContentSaveAction.PublishWithDescendants: case ContentSaveAction.PublishWithDescendantsNew: { @@ -1074,7 +1075,7 @@ public class ContentController : ContentControllerBase AddDomainWarnings(publishStatus, successfulCultures, globalNotifications); AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures); } - break; + break; case ContentSaveAction.PublishWithDescendantsForce: case ContentSaveAction.PublishWithDescendantsForceNew: { @@ -1090,7 +1091,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(); } @@ -2745,7 +2746,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 @@ -2773,7 +2774,7 @@ public class ContentController : ContentControllerBase } } } - break; + break; case PublishResultType.FailedPublishPathNotPublished: { //TODO: This doesn't take into account variations with the successfulCultures param @@ -2782,14 +2783,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 @@ -2798,7 +2799,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 @@ -2807,7 +2808,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 @@ -2816,7 +2817,7 @@ public class ContentController : ContentControllerBase _localizedTextService.Localize(null, "publish"), _localizedTextService.Localize("publish", "contentPublishedFailedIsTrashed", new[] { names }).Trim()); } - break; + break; case PublishResultType.FailedPublishContentInvalid: { if (successfulCultures == null) @@ -2840,7 +2841,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 36a60843fb..3a20f7a02d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -79,7 +79,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/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index fb8629c461..7cf6ba3cf5 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -278,6 +278,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute if (!authorizationResult.Succeeded) { + actionContext.Result = new ForbidResult(); return false; }