From 97e0c79d94b762d6731fdb1000209f84f9659b52 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 2 Oct 2025 20:10:16 +0200 Subject: [PATCH 001/155] Caching: Fixes regression of the caching of null representations for missing dictionary items (closes #20336 for 16) (#20349) * Ports fix to regression of the caching of null representations for missing dictionary items. * Fixed error raised in code review. --------- Co-authored-by: Kenn Jacobsen --- src/Umbraco.Core/Cache/AppCacheExtensions.cs | 38 ++++- .../Repositories/DictionaryRepositoryTest.cs | 145 +++++++++++++++++- 2 files changed, 176 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Cache/AppCacheExtensions.cs b/src/Umbraco.Core/Cache/AppCacheExtensions.cs index 480b677f2459..32c1b772f014 100644 --- a/src/Umbraco.Core/Cache/AppCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/AppCacheExtensions.cs @@ -41,27 +41,39 @@ public static void InsertCacheItem( public static T? GetCacheItem(this IAppCache provider, string cacheKey) { var result = provider.Get(cacheKey); - if (IsRetrievedItemNull(result)) + if (result == null) { return default; } + // If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string). + // Otherwise consider it a null value. + if (RetrievedNullRepresentationInCache(result)) + { + return RequestedNullRepresentationInCache() ? (T)result : default; + } + return result.TryConvertTo().Result; } public static T? GetCacheItem(this IAppCache provider, string cacheKey, Func getCacheItem) { var result = provider.Get(cacheKey, () => getCacheItem()); - if (IsRetrievedItemNull(result)) + if (result == null) { return default; } + // If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string). + // Otherwise consider it a null value. + if (RetrievedNullRepresentationInCache(result)) + { + return RequestedNullRepresentationInCache() ? (T)result : default; + } + return result.TryConvertTo().Result; } - private static bool IsRetrievedItemNull(object? result) => result is null or (object)Cms.Core.Constants.Cache.NullRepresentationInCache; - public static async Task GetCacheItemAsync( this IAppPolicyCache provider, string cacheKey, @@ -77,9 +89,25 @@ public static void InsertCacheItem( provider.Insert(cacheKey, () => result, timeout, isSliding); } - return result == null ? default : result.TryConvertTo().Result; + if (result == null) + { + return default; + } + + // If we've retrieved the specific string that represents null in the cache, return it only if we are requesting it (via a typed request for a string). + // Otherwise consider it a null value. + if (RetrievedNullRepresentationInCache(result)) + { + return RequestedNullRepresentationInCache() ? (T)result : default; + } + + return result.TryConvertTo().Result; } + private static bool RetrievedNullRepresentationInCache(object result) => result == (object)Cms.Core.Constants.Cache.NullRepresentationInCache; + + private static bool RequestedNullRepresentationInCache() => typeof(T) == typeof(string); + public static async Task InsertCacheItemAsync( this IAppPolicyCache provider, string cacheKey, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs index cb3414ad3aad..acdda16406a5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -1,13 +1,16 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -22,6 +25,16 @@ internal sealed class DictionaryRepositoryTest : UmbracoIntegrationTest private IDictionaryRepository CreateRepository() => GetRequiredService(); + private IDictionaryRepository CreateRepositoryWithCache(AppCaches cache) => + + // Create a repository with a real runtime cache. + new DictionaryRepository( + GetRequiredService(), + cache, + GetRequiredService>(), + GetRequiredService(), + GetRequiredService()); + [Test] public async Task Can_Perform_Get_By_Key_On_DictionaryRepository() { @@ -396,6 +409,134 @@ public void Can_Perform_GetDictionaryItemKeyMap_On_DictionaryRepository() } } + [Test] + public void Can_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_With_Cache() + { + var cache = AppCaches.Create(Mock.Of()); + var repository = CreateRepositoryWithCache(cache); + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + + // Modify the value directly in the database. This won't be reflected in the repository cache and hence if the cache + // is working as expected we should get the same value as above. + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Execute("UPDATE cmsLanguageText SET value = 'Read More (updated)' WHERE value = 'Read More' and LanguageId = 1"); + scope.Complete(); + } + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + + cache.IsolatedCaches.ClearCache(); + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More (updated)", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + } + + [Test] + public void Can_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_With_Cache() + { + var cache = AppCaches.Create(Mock.Of()); + var repository = CreateRepositoryWithCache(cache); + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNull(dictionaryItem); + } + + // Modify the value directly in the database such that it now exists. This won't be reflected in the repository cache and hence if the cache + // is working as expected we should get the same null value as above. + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Execute("UPDATE cmsDictionary SET [key] = 'Read More Updated' WHERE [key] = 'Read More'"); + scope.Complete(); + } + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNull(dictionaryItem); + } + + cache.IsolatedCaches.ClearCache(); + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNotNull(dictionaryItem); + } + } + + [Test] + public void Cannot_Perform_Cached_Request_For_Existing_Value_By_Key_On_DictionaryRepository_Without_Cache() + { + var repository = CreateRepository(); + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + + // Modify the value directly in the database. As we don't have caching enabled on the repository we should get the new value. + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Execute("UPDATE cmsLanguageText SET value = 'Read More (updated)' WHERE value = 'Read More' and LanguageId = 1"); + scope.Complete(); + } + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More"); + + Assert.AreEqual("Read More (updated)", dictionaryItem.Translations.Single(x => x.LanguageIsoCode == "en-US").Value); + } + } + + [Test] + public void Cannot_Perform_Cached_Request_For_NonExisting_Value_By_Key_On_DictionaryRepository_Without_Cache() + { + var repository = CreateRepository(); + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNull(dictionaryItem); + } + + // Modify the value directly in the database such that it now exists. As we don't have caching enabled on the repository we should get the new value. + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.Execute("UPDATE cmsDictionary SET [key] = 'Read More Updated' WHERE [key] = 'Read More'"); + scope.Complete(); + } + + using (ScopeProvider.CreateScope()) + { + var dictionaryItem = repository.Get("Read More Updated"); + + Assert.IsNotNull(dictionaryItem); + } + } + public async Task CreateTestData() { var languageService = GetRequiredService(); From d9592aa26d99d0c8298c218414e12ea479c5afa1 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 2 Oct 2025 21:16:25 +0200 Subject: [PATCH 002/155] Bumped version to 16.3.0-rc2. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 0b4f9c3997a6..a3c728eba2e7 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": "16.3.0-rc", + "version": "16.3.0-rc2", "assemblyVersion": { "precision": "build" }, From bfd2594c7be86295f46ae5022498b7dd1e6d87db Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 6 Oct 2025 21:20:16 +0200 Subject: [PATCH 003/155] Hybrid cache: Check for `ContentCacheNode` instead of object on exists for hybrid cache to ensure correct deserialization (closes #20352) (#20383) Checked for ContentCacheNode instead of object on exists for hybrid cache to ensure correct deserialization. (cherry picked from commit 184c17e2c8a5f45557bfe66ee1444793a55d46d0) --- .../Extensions/HybridCacheExtensions.cs | 4 ++-- .../Services/DocumentCacheService.cs | 4 ++-- .../Services/MediaCacheService.cs | 4 ++-- .../Extensions/HybridCacheExtensionsTests.cs | 13 +++++++------ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs index 427bc67d3f52..ee1b7aefda05 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs @@ -17,9 +17,9 @@ internal static class HybridCacheExtensions /// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191 /// Will never add or alter the state of any items in the cache. /// - public static async Task ExistsAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key) + public static async Task ExistsAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key) { - (bool exists, _) = await TryGetValueAsync(cache, key); + (bool exists, _) = await TryGetValueAsync(cache, key); return exists; } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index 2d41bc0a1261..1675cb05cfb2 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -205,7 +205,7 @@ public async Task SeedAsync(CancellationToken cancellationToken) var cacheKey = GetCacheKey(key, false); - var existsInCache = await _hybridCache.ExistsAsync(cacheKey); + var existsInCache = await _hybridCache.ExistsAsync(cacheKey); if (existsInCache is false) { uncachedKeys.Add(key); @@ -278,7 +278,7 @@ public async Task HasContentByIdAsync(int id, bool preview = false) return false; } - return await _hybridCache.ExistsAsync(GetCacheKey(keyAttempt.Result, preview)); + return await _hybridCache.ExistsAsync(GetCacheKey(keyAttempt.Result, preview)); } public async Task RefreshContentAsync(IContent content) diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs index 29095b1d0433..65b8f91945a5 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs @@ -133,7 +133,7 @@ public async Task HasContentByIdAsync(int id) return false; } - return await _hybridCache.ExistsAsync($"{keyAttempt.Result}"); + return await _hybridCache.ExistsAsync($"{keyAttempt.Result}"); } public async Task RefreshMediaAsync(IMedia media) @@ -170,7 +170,7 @@ public async Task SeedAsync(CancellationToken cancellationToken) var cacheKey = GetCacheKey(key, false); - var existsInCache = await _hybridCache.ExistsAsync(cacheKey); + var existsInCache = await _hybridCache.ExistsAsync(cacheKey); if (existsInCache is false) { uncachedKeys.Add(key); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs index 27e0cb0d0ad5..152fe28b4ef6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Caching.Hybrid; using Moq; using NUnit.Framework; +using Umbraco.Cms.Infrastructure.HybridCache; using Umbraco.Cms.Infrastructure.HybridCache.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.HybridCache.Extensions; @@ -27,20 +28,20 @@ public async Task ExistsAsync_WhenKeyExists_ShouldReturnTrue() { // Arrange string key = "test-key"; - var expectedValue = "test-value"; + var expectedValue = new ContentCacheNode { Id = 1234 }; _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, null!, - It.IsAny>>(), + It.IsAny>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(expectedValue); // Act - var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key); + var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key); // Assert Assert.IsTrue(exists); @@ -56,14 +57,14 @@ public async Task ExistsAsync_WhenKeyDoesNotExist_ShouldReturnFalse() .Setup(cache => cache.GetOrCreateAsync( key, null!, - It.IsAny>>(), + It.IsAny>>(), It.IsAny(), null, CancellationToken.None)) .Returns(( string key, object? state, - Func> factory, + Func> factory, HybridCacheEntryOptions? options, IEnumerable? tags, CancellationToken token) => @@ -72,7 +73,7 @@ public async Task ExistsAsync_WhenKeyDoesNotExist_ShouldReturnFalse() }); // Act - var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key); + var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key); // Assert Assert.IsFalse(exists); From 629e9051870ba4cd5e5e539b96fae91e0cd628a2 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:21:14 +0200 Subject: [PATCH 004/155] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index a3c728eba2e7..cf259974301b 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": "16.3.0-rc2", + "version": "16.3.0-rc3", "assemblyVersion": { "precision": "build" }, From b036eb3a759ba1972b3b0e8e0a6b234f98b1e8ae Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 8 Oct 2025 07:56:49 +0200 Subject: [PATCH 005/155] Performance: Added request cache to media type retrieval in media picker validation (#20405) * Added request cache to media type retrieval in media picker validation. * Applied suggestions from code review. --- .../MediaPicker3PropertyEditor.cs | 26 +++++++++++++++---- ...ataValueReferenceFactoryCollectionTests.cs | 3 ++- .../MediaPicker3ValueEditorValidationTests.cs | 3 ++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index ae43c493efaf..26efa02f1029 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -82,7 +82,8 @@ public MediaPicker3PropertyValueEditor( IDataTypeConfigurationCache dataTypeReadCache, ILocalizedTextService localizedTextService, IMediaTypeService mediaTypeService, - IMediaNavigationQueryService mediaNavigationQueryService) + IMediaNavigationQueryService mediaNavigationQueryService, + AppCaches appCaches) : base(shortStringHelper, jsonSerializer, ioHelper, attribute) { _jsonSerializer = jsonSerializer; @@ -95,7 +96,7 @@ public MediaPicker3PropertyValueEditor( var validators = new TypedJsonValidatorRunner, MediaPicker3Configuration>( jsonSerializer, new MinMaxValidator(localizedTextService), - new AllowedTypeValidator(localizedTextService, mediaTypeService, _mediaService), + new AllowedTypeValidator(localizedTextService, mediaTypeService, _mediaService, appCaches), new StartNodeValidator(localizedTextService, mediaNavigationQueryService)); Validators.Add(validators); @@ -401,18 +402,22 @@ public IEnumerable Validate( /// internal sealed class AllowedTypeValidator : ITypedJsonValidator, MediaPicker3Configuration> { + private const string MediaTypeCacheKeyFormat = nameof(AllowedTypeValidator) + "_MediaTypeKey_{0}"; + private readonly ILocalizedTextService _localizedTextService; private readonly IMediaTypeService _mediaTypeService; private readonly IMediaService _mediaService; + private readonly AppCaches _appCaches; /// /// Initializes a new instance of the class. /// - public AllowedTypeValidator(ILocalizedTextService localizedTextService, IMediaTypeService mediaTypeService, IMediaService mediaService) + public AllowedTypeValidator(ILocalizedTextService localizedTextService, IMediaTypeService mediaTypeService, IMediaService mediaService, AppCaches appCaches) { _localizedTextService = localizedTextService; _mediaTypeService = mediaTypeService; _mediaService = mediaService; + _appCaches = appCaches; } /// @@ -452,9 +457,20 @@ public IEnumerable Validate( foreach (var typeAlias in distinctTypeAliases) { - IMediaType? type = _mediaTypeService.Get(typeAlias); + // Cache media type lookups since the same media type is likely to be used multiple times in validation, + // particularly if we have multiple languages and blocks. + var cacheKey = string.Format(MediaTypeCacheKeyFormat, typeAlias); + string? typeKey = _appCaches.RequestCache.GetCacheItem(cacheKey); + if (typeKey is null) + { + typeKey = _mediaTypeService.Get(typeAlias)?.Key.ToString(); + if (typeKey is not null) + { + _appCaches.RequestCache.Set(cacheKey, typeKey); + } + } - if (type is null || allowedTypes.Contains(type.Key.ToString()) is false) + if (typeKey is null || allowedTypes.Contains(typeKey) is false) { return [ diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs index 953301f74f50..a5fed60fec25 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -41,7 +41,8 @@ public class DataValueReferenceFactoryCollectionTests Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of())); + Mock.Of(), + AppCaches.Disabled)); private IIOHelper IOHelper { get; } = Mock.Of(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs index 6816b1761504..8bd7f3b18790 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs @@ -218,7 +218,8 @@ private static (MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor Value Mock.Of(), Mock.Of(), mediaTypeServiceMock.Object, - mediaNavigationQueryServiceMock.Object) + mediaNavigationQueryServiceMock.Object, + AppCaches.Disabled) { ConfigurationObject = new MediaPicker3Configuration() }; From dab9df3f1089f8144614bc1f5465b91fff84c0c4 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 8 Oct 2025 07:59:10 +0200 Subject: [PATCH 006/155] Bumped version to 16.3.0-rc4. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index cf259974301b..cd1e47b49d76 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": "16.3.0-rc3", + "version": "16.3.0-rc4", "assemblyVersion": { "precision": "build" }, From d6ce8d91a9370cd70483182a5c8f830b6afcbf2b Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:24:58 +0200 Subject: [PATCH 007/155] PropertyType workspace: layout & labeling adjustments (#20131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Property workspace update * Fixed error with updating the properties * Unused variable * Added data-mark to description textarea * make select 100% width * tiny appearance-option style adjustments * Make placeholder property inside the input-with-alias optional * Moving variations and member type option to their own boxes --------- Co-authored-by: Mads Rasmussen Co-authored-by: Niels Lyngsø Co-authored-by: Niels Lyngsø --- .../src/assets/lang/en.ts | 4 + .../src/assets/lang/es.ts | 9 + ...roperty-workspace-view-settings.element.ts | 389 ++++++++---------- .../input-with-alias.element.ts | 7 +- .../property-layout.element.ts | 6 +- .../package-lock.json | 52 +-- .../Umbraco.Tests.AcceptanceTest/package.json | 4 +- .../Content/ContentWithImageCropper.spec.ts | 3 +- .../Settings/PartialView/PartialView.spec.ts | 3 +- 9 files changed, 224 insertions(+), 253 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 9d1074a2d5d6..e134a06692b7 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -849,6 +849,7 @@ export default { delete: 'Delete', deleted: 'Deleted', deleting: 'Deleting...', + description: 'Description', design: 'Design', details: 'Details', dictionary: 'Dictionary', @@ -923,6 +924,7 @@ export default { pleasewait: 'One moment please...', previous: 'Previous', properties: 'Properties', + propertyEditor: 'Property editor', readMore: 'Read more', rebuild: 'Rebuild', reciept: 'Email to receive form data', @@ -2197,9 +2199,11 @@ export default { validateAsEmail: 'Validate as an email address', validateAsNumber: 'Validate as a number', validateAsUrl: 'Validate as a URL', + validationPattern: 'Validation pattern', enterCustomValidation: '...or enter a custom validation', fieldIsMandatory: 'Field is mandatory', mandatoryMessage: 'Enter a custom validation error message (optional)', + mandatoryMessageLabel: 'Feedback message', validationRegExp: 'Enter a regular expression', validationRegExpMessage: 'Enter a custom validation error message (optional)', minCount: 'You need to add at least', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/es.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/es.ts index 581c8b1a7c2b..06c9716958f9 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/es.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/es.ts @@ -481,6 +481,7 @@ export default { deleted: 'Borrado', deleting: 'Borrando...', design: 'Diseño', + description: 'Descripción', dictionary: 'Diccionario', dimensions: 'Dimensiones', dividerPosition: (value: string | number) => `Divisor en ${value}%`, @@ -525,6 +526,7 @@ export default { off: 'Desactivado', ok: 'OK', open: 'Abrir', + options: 'Opciones', on: 'Activado', or: 'o', orderBy: 'Ordenar por', @@ -533,6 +535,7 @@ export default { pleasewait: 'Un momento por favor...', previous: 'Anterior', properties: 'Propiedades', + propertyEditor: 'Editor de propiedades', reciept: 'Mail para recibir los datos del formulario', recycleBin: 'Papelera', recycleBinEmpty: 'Tu papelera está vacía', @@ -1153,6 +1156,9 @@ export default { memberCanEdit: 'Miembro puede editar', showOnMemberProfile: 'Mostrar en perfil de miembro', tabHasNoSortOrder: 'pestaña no tiene orden', + displaySettingsHeadline: 'Apariencia', + displaySettingsLabelOnLeft: 'Etiqueta a la izquierda', + displaySettingsLabelOnTop: 'Etiqueta arriba (ancho completo)', }, languages: { addLanguage: 'Agregar idioma', @@ -1377,9 +1383,12 @@ export default { validateAsEmail: 'Validar como email', validateAsNumber: 'Validar como número', validateAsUrl: 'Validar como URL', + validationPattern: 'Patrón de validación', enterCustomValidation: '...o introduce tu propia validación', + customValidation: 'Validación personalizada', fieldIsMandatory: 'Campo obligatorio', validationRegExp: 'Introduce una expresión regular', + mandatoryMessageLabel: 'Mensaje de validación', minCount: 'Necesitas añadir al menos', maxCount: 'Sólo puedes tener', items: 'elementos', diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/views/settings/property-workspace-view-settings.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/views/settings/property-workspace-view-settings.element.ts index b12707f699a3..b7bd7940791f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/views/settings/property-workspace-view-settings.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/views/settings/property-workspace-view-settings.element.ts @@ -1,18 +1,13 @@ import { UMB_PROPERTY_TYPE_WORKSPACE_CONTEXT } from '../../../index.js'; -import { css, html, customElement, state, nothing, query } from '@umbraco-cms/backoffice/external/lit'; -import { generateAlias } from '@umbraco-cms/backoffice/utils'; -import { umbBindToValidation } from '@umbraco-cms/backoffice/validation'; +import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, umbBindToValidation } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content-type'; import type { UmbPropertyTypeScaffoldModel } from '@umbraco-cms/backoffice/content-type'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; -import type { - UUIBooleanInputEvent, - UUIInputEvent, - UUIInputLockElement, - UUISelectEvent, -} from '@umbraco-cms/backoffice/external/uui'; +import type { UUIBooleanInputEvent, UUIInputEvent, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components'; @customElement('umb-property-type-workspace-view-settings') export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement implements UmbWorkspaceViewElement { @@ -46,40 +41,25 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i @state() private _data?: UmbPropertyTypeScaffoldModel; - @state() - private _aliasLocked = true; - - @state() - private _autoGenerateAlias = true; - @state() private _contentTypeVariesByCulture?: boolean; @state() private _contentTypeVariesBySegment?: boolean; - @query('#alias-input') - private _aliasInput!: UUIInputLockElement; - @state() private _entityType?: string; + @state() + private _isNew?: boolean; + constructor() { super(); this.consumeContext(UMB_PROPERTY_TYPE_WORKSPACE_CONTEXT, (instance) => { this.#context = instance; - this.observe( - instance?.data, - (data) => { - if (!this._data && data?.alias) { - // Initial. Loading existing property - this._autoGenerateAlias = false; - } - this._data = data; - }, - 'observeData', - ); + this.observe(instance?.data, (data) => (this._data = data), 'observeData'); + this.observe(instance?.isNew, (isNew) => (this._isNew = isNew), '_observeIsNew'); }); this.consumeContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT, (instance) => { @@ -99,19 +79,6 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i this.#context?.updateData(partialValue); } - #onNameChange(event: UUIInputEvent) { - this.updateValue({ name: event.target.value.toString() }); - if (this._aliasLocked && this._autoGenerateAlias) { - this.updateValue({ alias: generateAlias(this._data?.name ?? '') }); - } - } - - #onAliasChange() { - // TODO: Why can I not get the correct value via event? Is it an issue in uui library too? - const alias = generateAlias(this._aliasInput.value.toString()); - this.updateValue({ alias }); - } - #onDescriptionChange(event: UUIInputEvent) { this.updateValue({ description: event.target.value.toString() }); } @@ -166,16 +133,6 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i this.updateValue({ isSensitive: e.target.checked }); } - #onToggleAliasLock() { - this._aliasLocked = !this._aliasLocked; - if (this._aliasLocked && !this._data?.alias) { - // Reenable auto-generate if alias is empty and locked. - this._autoGenerateAlias = true; - } else { - this._autoGenerateAlias = false; - } - } - #onCustomValidationChange(event: UUISelectEvent) { const value = event.target.value.toString(); const regEx = value !== '!NOVALIDATION!' ? value : null; @@ -217,134 +174,137 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i this.updateValue({ variesBySegment: !sharedAcrossSegments }); } + #onNameAndAliasChange(event: InputEvent & { target: UmbInputWithAliasElement }) { + const name = event.target.value ?? ''; + const alias = event.target.alias ?? ''; + this.updateValue({ name, alias }); + } + override render() { if (!this._data) return; return html` -
- - - - - - - - - + + + + + + -
- + + + - -
-
- Validation - ${this.#renderMandatory()} -

- Custom validation -

- ${this.#renderCustomValidation()} -
- ${this.#renderVariationControls()} - -
${this.#renderAlignLeftIcon()} ${this.#renderAlignTopIcon()}
+
+ + + Validation + ${this.#renderMandatory()} ${this.#renderCustomValidation()} - ${this.#renderMemberTypeOptions()} + ${this.#renderVariationControls()} + + + Appearance + +
${this.#renderAlignLeftIcon()} ${this.#renderAlignTopIcon()}
+
+ + ${this.#renderMemberTypeOptions()} `; } #renderMemberTypeOptions() { if (this._entityType !== 'member-type') return nothing; - return html`
-
- - Options - -
+ return html` +
+ - + Allow this property value to be displayed on the member profile page + + - + Allow this property value to be edited by the member on their profile page + + - + Hide this property value from content editors that don't have access to view sensitive information -
-
`; + +
+ `; } #renderAlignLeftIcon() { - return html` - `; + `; } #renderAlignTopIcon() { - return html` + return html`
- `; + + +
`; } #renderMandatory() { - return html`
- + return html` -
+ slot="editor" + >Field is mandatory + ${this._data?.validation?.mandatory - ? html`` - : ''}`; + ? html`` + : ''} `; } #renderCustomValidation() { - return html` + return html` ${this._data?.validation?.regEx !== null - ? html` - - - ` - : nothing} `; + ? html` + + ` + : nothing}`; } #renderVariationControls() { return this._contentTypeVariesByCulture || this._contentTypeVariesBySegment - ? html` - - ${this._contentTypeVariesByCulture ? this.#renderVaryByCulture() : nothing} - ${this._contentTypeVariesBySegment ? this.#renderVaryBySegment() : nothing} - - ` + ? html` + Variation + + ${this._contentTypeVariesByCulture ? this.#renderVaryByCulture() : nothing} + ${this._contentTypeVariesBySegment ? this.#renderVaryBySegment() : nothing} + + ` : ''; } @@ -426,7 +396,7 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i + label=${this.localize.term('contentTypeEditor_cultureInvariantLabel')}> `; } @@ -437,7 +407,7 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i + label=${this.localize.term('contentTypeEditor_segmentInvariantLabel')}> `; } @@ -446,40 +416,31 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i UmbTextStyles, css` :host { - display: block; + display: flex; + flex-direction: column; + gap: var(--uui-size-layout-1); padding: var(--uui-size-layout-1); } - #alias-input, - #label-input, - #description-input { - width: 100%; + umb-property-layout[orientation='vertical'] { + padding: var(--uui-size-space-2) 0; } - #alias-input { - border-color: transparent; - background: var(--uui-color-surface); + umb-property-layout:first-of-type { + padding-top: 0; } - - #label-input { - font-weight: bold; /* TODO: UUI Input does not support bold text yet */ - --uui-input-border-color: transparent; - } - #label-input input { - font-weight: bold; - --uui-input-border-color: transparent; + umb-property-layout:last-of-type { + padding-bottom: 0; } - #description-input { - --uui-textarea-border-color: transparent; - font-weight: 0.5rem; /* TODO: Cant change font size of UUI textarea yet */ + uui-select { + width: 100%; } #appearances { display: flex; - gap: var(--uui-size-layout-1); - max-width: 350px; - margin: 0 auto; + gap: var(--uui-size-space-4); } + .appearance { position: relative; display: flex; @@ -493,37 +454,44 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i justify-content: space-between; gap: var(--uui-size-space-3); } + + .appearance-option { + display: flex; + width: 100%; + flex-direction: column; + gap: var(--uui-size-space-2); + } + .appearance-label { font-size: 0.8rem; line-height: 1; - font-weight: bold; + text-align: center; pointer-events: none; } - .appearance.left { - flex-grow: 1; - } - .appearance.top { - flex-shrink: 1; - } + .appearance svg { display: flex; width: 100%; color: var(--uui-color-text); } + .appearance:not(.selected):hover { border-color: var(--uui-color-border-emphasis); cursor: pointer; opacity: 1; } + .appearance.selected { background-color: var(--uui-color-surface); border-color: var(--uui-color-selected); color: var(--uui-color-selected); opacity: 1; } + .appearance.selected svg { color: var(--uui-color-selected); } + .appearance.selected::after { content: ''; position: absolute; @@ -532,30 +500,19 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i opacity: 0.1; background-color: var(--uui-color-selected); } - hr { - border: none; - border-top: 1px solid var(--uui-color-divider); - margin-top: var(--uui-size-space-6); - margin-bottom: var(--uui-size-space-5); - } + uui-input { width: 100%; } + uui-input:focus-within { z-index: 1; } - uui-input-lock:focus-within { - z-index: 1; - } + .container { display: flex; flex-direction: column; } - uui-form, - form { - display: block; - height: 100%; - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-with-alias/input-with-alias.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-with-alias/input-with-alias.element.ts index 4e8094719bc7..50818ac19c38 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-with-alias/input-with-alias.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-with-alias/input-with-alias.element.ts @@ -1,4 +1,4 @@ -import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; import { generateAlias } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; @@ -13,6 +13,9 @@ export class UmbInputWithAliasElement extends UmbFormControlMixin div { grid-column: span 2; } + /*@container (width > 600px) {*/ :host(:not([orientation='vertical'])) > div { grid-column: span 1; diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index b1e7b97b7a08..d6c157d75fd8 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,13 +8,13 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.40", - "@umbraco/playwright-testhelpers": "^16.0.50", + "@umbraco/playwright-testhelpers": "^17.0.0-beta.1", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" }, "devDependencies": { - "@playwright/test": "^1.50", + "@playwright/test": "1.50", "@types/node": "^20.9.0", "prompt": "^1.2.0", "tslib": "^2.4.0", @@ -32,13 +32,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", - "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", + "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.50.0" + "playwright": "1.50.1" }, "bin": { "playwright": "cli.js" @@ -48,13 +48,13 @@ } }, "node_modules/@types/node": { - "version": "20.17.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", - "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", + "version": "20.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", + "integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@umbraco/json-models-builders": { @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "16.0.50", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.50.tgz", - "integrity": "sha512-2tHQUdv8lCV4O47hGMvyC+JJfG4//fSSE/gUOVfFNDEHzhU5mSZ8f9oGKob8XOv2RB7ynZFYtIIxEMZIWrGFDA==", + "version": "17.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.0-beta.1.tgz", + "integrity": "sha512-EhS4j5ARDcR3tI7ArTmLuBHW+e49qyWq3Ts8ckCXvFjkPgR3u/Z5JPOIFWUZ+rTahNZi3axs3i+dVcWWA4Fyjw==", "license": "MIT", "dependencies": { "@umbraco/json-models-builders": "2.0.40", @@ -112,9 +112,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -189,13 +189,13 @@ } }, "node_modules/playwright": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", - "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", + "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.0" + "playwright-core": "1.50.1" }, "bin": { "playwright": "cli.js" @@ -208,9 +208,9 @@ } }, "node_modules/playwright-core": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", - "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", + "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -298,9 +298,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 6deebfb3fdcb..49cc458287de 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -14,7 +14,7 @@ "releaseTest": "npx playwright test DefaultConfig --grep \"@release\"" }, "devDependencies": { - "@playwright/test": "^1.50", + "@playwright/test": "1.50", "@types/node": "^20.9.0", "prompt": "^1.2.0", "tslib": "^2.4.0", @@ -22,7 +22,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.40", - "@umbraco/playwright-testhelpers": "^16.0.50", + "@umbraco/playwright-testhelpers": "^17.0.0-beta.1", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts index 717fc227c30f..a62f7b004e43 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts @@ -74,7 +74,8 @@ test('can publish content with the image cropper data type', {tag: '@smoke'}, as expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint); }); -test('can create content with the custom image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +// TODO: Fix flaky test +test.fixme('can create content with the custom image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange const customDataTypeName = 'CustomImageCropper'; const cropAlias = 'TestCropLabel'; diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts index a4e933b59eaa..27eadd1b46bb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/PartialView/PartialView.spec.ts @@ -65,7 +65,8 @@ test('can create a partial view from snippet', async ({umbracoApi, umbracoUi}) = await umbracoUi.partialView.isPartialViewRootTreeItemVisible(partialViewFileName); }); -test('can rename a partial view', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +// TODO: Fix flaky test +test.fixme('can rename a partial view', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange const wrongPartialViewName = 'WrongName'; const wrongPartialViewFileName = wrongPartialViewName + '.cshtml'; From 16132b0075391955f5e1c29d6b437c5e6fcef957 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 9 Oct 2025 06:37:21 +0200 Subject: [PATCH 008/155] Update Umbraco version for LTS release in template --- templates/UmbracoProject/.template.config/template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index fe8b91ba18ea..37aa21d96685 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -98,7 +98,7 @@ }, { "condition": "(UmbracoRelease == 'LTS')", - "value": "13.10.1" + "value": "13.11.0" } ] } From 64836e0b7af02a87e162946575807e0c4e921563 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 9 Oct 2025 08:54:36 +0200 Subject: [PATCH 009/155] Update version from 17.0.0-rc to 17.1.0-rc --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index be8cef3c1068..2ba7176858c0 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": "17.0.0-rc", + "version": "17.1.0-rc", "assemblyVersion": { "precision": "build" }, From 1fe7931d0766e032dfdefd3028c055b60cbd595d Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:41:41 +0200 Subject: [PATCH 010/155] Migrations: Adjust the `JsonBlockValueConverter` to handle conflicts with 'values' property (#20429) * Adjust the `JsonBlockValueConverter` to handle conflicts with 'values' property (due to old data schema) * Simplify code * Add unit test to verify change. --------- Co-authored-by: Andy Butland --- .../Serialization/JsonBlockValueConverter.cs | 48 ++++++++++++++++++- .../JsonBlockValueConverterTests.cs | 38 +++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs index 71624b6529b0..b29e829266fe 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Extensions; @@ -121,7 +122,17 @@ private static Type GetLayoutItemType(Type blockValueType) } private List DeserializeBlockItemData(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName) - => DeserializeListOf(ref reader, options, typeToConvert, propertyName); + { + try + { + return DeserializeListOf(ref reader, options, typeToConvert, propertyName); + } + catch (JsonException ex) when (ex.Path?.EndsWith(".values") is true) + { + // If we hit a JsonException due to the "values" property conflict, attempt the fallback deserialization + return FallbackBlockItemDataDeserialization(ref reader, options); + } + } private List DeserializeBlockVariation(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName) => DeserializeListOf(ref reader, options, typeToConvert, propertyName); @@ -224,5 +235,38 @@ private void DeserializeAndSetLayout(ref Utf8JsonReader reader, JsonSerializerOp } } } -} + [Obsolete("Only needed to support the old data schema. Remove in V18.")] + private static List FallbackBlockItemDataDeserialization(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + JsonArray? arrayElement = JsonSerializer.Deserialize(ref reader, options); + + return arrayElement? + .Select(itemElement => DeserializeBlockItemData(itemElement, options)) + .OfType() + .ToList() ?? []; + } + + [Obsolete("Only needed to support the old data schema. Remove in V18.")] + private static BlockItemData? DeserializeBlockItemData(JsonNode? jsonNode, JsonSerializerOptions options) + { + if (jsonNode is not JsonObject jsonObject || jsonObject.ContainsKey("values") is false) + { + // Nothing to be done, just deserialize as usual + return jsonNode.Deserialize(options); + } + + // Handle the "values" property conflict by extracting the "values" property first and adding it to the + // RawPropertyValues dictionary after deserialization + JsonNode? values = jsonObject["values"]; + jsonObject.Remove("values"); + + BlockItemData? blockItemData = jsonObject.Deserialize(options); + if (blockItemData is not null) + { + blockItemData.RawPropertyValues["values"] = values.Deserialize(options); + } + + return blockItemData; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs index f48c286a91e0..4d76b2ad8731 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs @@ -470,4 +470,42 @@ public void Try_Deserialize_Unknown_Block_Layout_With_Nested_Array() var serializer = new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory()); Assert.DoesNotThrow(() => serializer.Deserialize(json)); } + + /// + /// Test case that verifies the fix for https://github.com/umbraco/Umbraco-CMS/issues/20409. + /// + [Test] + public void Can_Deserialize_BlockGrid_With_Blocks_Using_Values_As_Property_Alias() + { + // Create a serialized BlockGridValue in Umbraco 13 format that has a block with a property alias "values". + var serialized = @"{ + ""layout"":{ + ""Umbraco.BlockList"":[ + { + ""contentUdi"":""umb://element/6ad18441631140d48515ea0fc5b00425"" + } + ] + }, + ""contentData"":[ + { + ""contentTypeKey"":""a1d1123c-289b-4a05-b33f-9f06cb723da1"", + ""udi"":""umb://element/6ad18441631140d48515ea0fc5b00425"", + ""text"":""Text"", + ""values"":""Values"" + } + ], + ""settingsData"":[ + ] +}"; + + var serializer = new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory()); + var deserialized = serializer.Deserialize(serialized); + + Assert.IsNotNull(deserialized); + + Assert.AreEqual(1, deserialized.ContentData.Count); + Assert.AreEqual(2, deserialized.ContentData[0].RawPropertyValues.Count); + Assert.AreEqual("Text", deserialized.ContentData[0].RawPropertyValues["text"]); + Assert.AreEqual("Values", deserialized.ContentData[0].RawPropertyValues["values"]); + } } From 767894b72303c54084f7a21bbea077a3fad31738 Mon Sep 17 00:00:00 2001 From: Erik Leusink <49444441+Programeerik@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:50:11 +0200 Subject: [PATCH 011/155] Color Picker: Validate uniqueness of selected colors (#20431) * Added unique color checker to color picker. * Added Unittest for duplicates * optimized for codescene * removed the bump and simplified the function * Fixed behaviour for duplicate checks so unit test passes. A little refactoring. * Adds continue so invalid colors aren't checked for duplicates. --------- Co-authored-by: Andy Butland --- .../ColorPickerConfigurationEditor.cs | 53 +++++++++++++++++-- .../PropertyEditors/ColorListValidatorTest.cs | 20 +++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index fe419a6566c2..f1da6618487f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -10,8 +10,11 @@ namespace Umbraco.Cms.Core.PropertyEditors; -internal sealed class ColorPickerConfigurationEditor : ConfigurationEditor +internal sealed partial class ColorPickerConfigurationEditor : ConfigurationEditor { + /// + /// Initializes a new instance of the class. + /// public ColorPickerConfigurationEditor(IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) : base(ioHelper) { @@ -19,13 +22,17 @@ public ColorPickerConfigurationEditor(IIOHelper ioHelper, IConfigurationEditorJs items.Validators.Add(new ColorListValidator(configurationEditorJsonSerializer)); } - internal sealed class ColorListValidator : IValueValidator + internal sealed partial class ColorListValidator : IValueValidator { private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + /// + /// Initializes a new instance of the class. + /// public ColorListValidator(IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) => _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + /// public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext) { var stringValue = value?.ToString(); @@ -46,17 +53,53 @@ public IEnumerable Validate(object? value, string? valueType, if (items is null) { - yield return new ValidationResult($"The configuration value {stringValue} is not a valid color picker configuration", new[] { "items" }); + yield return new ValidationResult($"The configuration value {stringValue} is not a valid color picker configuration", ["items"]); yield break; } + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var duplicates = new List(); foreach (ColorPickerConfiguration.ColorPickerItem item in items) { - if (Regex.IsMatch(item.Value, "^([0-9a-f]{3}|[0-9a-f]{6})$", RegexOptions.IgnoreCase) == false) + if (ColorPattern().IsMatch(item.Value) == false) { - yield return new ValidationResult($"The value {item.Value} is not a valid hex color", new[] { "items" }); + yield return new ValidationResult($"The value {item.Value} is not a valid hex color", ["items"]); + continue; + } + + var normalized = Normalize(item.Value); + if (seen.Add(normalized) is false) + { + duplicates.Add(normalized); } } + + if (duplicates.Count > 0) + { + yield return new ValidationResult( + $"Duplicate color values are not allowed: {string.Join(", ", duplicates)}", + ["items"]); + } } + + private static string Normalize(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var normalizedValue = value.Trim().ToLowerInvariant(); + + if (normalizedValue.Length == 3) + { + normalizedValue = $"{normalizedValue[0]}{normalizedValue[0]}{normalizedValue[1]}{normalizedValue[1]}{normalizedValue[2]}{normalizedValue[2]}"; + } + + return normalizedValue; + } + + [GeneratedRegex("^([0-9a-f]{3}|[0-9a-f]{6})$", RegexOptions.IgnoreCase, "en-GB")] + private static partial Regex ColorPattern(); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs index 72de48631f90..01b8a428c39b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ColorListValidatorTest.cs @@ -58,4 +58,24 @@ public void Validates_Color_Vals() PropertyValidationContext.Empty()); Assert.AreEqual(2, result.Count()); } + + [Test] + public void Validates_Color_Vals_Are_Unique() + { + var validator = new ColorPickerConfigurationEditor.ColorListValidator(ConfigurationEditorJsonSerializer()); + var result = + validator.Validate( + new JsonArray( + JsonNode.Parse("""{"value": "FFFFFF", "label": "One"}"""), + JsonNode.Parse("""{"value": "000000", "label": "Two"}"""), + JsonNode.Parse("""{"value": "FF00AA", "label": "Three"}"""), + JsonNode.Parse("""{"value": "fff", "label": "Four"}"""), + JsonNode.Parse("""{"value": "000000", "label": "Five"}"""), + JsonNode.Parse("""{"value": "F0A", "label": "Six"}""")), + null, + null, + PropertyValidationContext.Empty()); + Assert.AreEqual(1, result.Count()); + Assert.IsTrue(result.First().ErrorMessage.Contains("ffffff, 000000, ff00aa")); + } } From bcedc8de2a40ff6721061ff5d939bdee47ce0519 Mon Sep 17 00:00:00 2001 From: Rick Butterfield Date: Thu, 9 Oct 2025 13:27:53 +0100 Subject: [PATCH 012/155] Emails: Add `Expires` header (#20285) * Add `Expiry` header to emails, set default expiry to 30 days and allow user config via `appsettings` * Remove `IsSmtpExpirationConfigured` as it will always have a value * Check for `emailExpiration` value * Removed `EmailExpiration` default value as it should be opt-in * Simplify SMTP email expiration condition * Fix APICompat issue * Add implementation to `NotImplementedEmailSender` * Rename `emailExpiration` to `expires` to match the SMTP header * Obsolete interfaces without `expires` parameter, delegate to an existing method. * Set expiry TimeSpan values from user configurable settings with defaults * Fix formating * Handle breaking changes, add obsoletion messages and simplify interfaces. * Fix default of invite expires timespan (was being parsed as 72 days not 72 hours). --------- Co-authored-by: Andy Butland --- .../Configuration/Models/GlobalSettings.cs | 5 +++ .../Configuration/Models/SecuritySettings.cs | 15 ++++++++ .../Configuration/Models/SmtpSettings.cs | 5 +++ .../EmailNotificationMethod.cs | 2 +- src/Umbraco.Core/Mail/IEmailSender.cs | 19 ++++++++++ .../Mail/NotImplementedEmailSender.cs | 4 +++ .../Services/NotificationService.cs | 6 ++-- .../Mail/BasicSmtpEmailSenderClient.cs | 26 +++++++++++--- .../Mail/EmailSender.cs | 36 +++++++++++-------- .../Mail/Interfaces/IEmailSenderClient.cs | 18 +++++++--- .../Security/EmailUserForgotPasswordSender.cs | 2 +- .../Security/EmailUserInviteSender.cs | 22 ++++++++++-- 12 files changed, 130 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index d02555c850b5..8bd2e04dae79 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -181,6 +181,11 @@ internal const string /// public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host); + /// + /// Gets a value indicating whether SMTP expiry is configured. + /// + public bool IsSmtpExpiryConfigured => Smtp?.EmailExpiration != null && Smtp?.EmailExpiration.HasValue == true; + /// /// Gets a value indicating whether there is a physical pickup directory configured. /// diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index bfff570c4f7c..0602ab6e8ea3 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -34,6 +34,9 @@ public class SecuritySettings internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout"; internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error"; + internal const string StaticPasswordResetEmailExpiry = "01:00:00"; + internal const string StaticUserInviteEmailExpiry = "3.00:00:00"; + /// /// Gets or sets a value indicating whether to keep the user logged in. /// @@ -159,4 +162,16 @@ public class SecuritySettings /// [DefaultValue(StaticAuthorizeCallbackErrorPathName)] public string AuthorizeCallbackErrorPathName { get; set; } = StaticAuthorizeCallbackErrorPathName; + + /// + /// Gets or sets the expiry time for password reset emails. + /// + [DefaultValue(StaticPasswordResetEmailExpiry)] + public TimeSpan PasswordResetEmailExpiry { get; set; } = TimeSpan.Parse(StaticPasswordResetEmailExpiry); + + /// + /// Gets or sets the expiry time for user invite emails. + /// + [DefaultValue(StaticUserInviteEmailExpiry)] + public TimeSpan UserInviteEmailExpiry { get; set; } = TimeSpan.Parse(StaticUserInviteEmailExpiry); } diff --git a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs index 92229b1b6d08..ea56445aa28e 100644 --- a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs @@ -96,4 +96,9 @@ public class SmtpSettings : ValidatableEntryBase /// Gets or sets a value for the SMTP password. /// public string? Password { get; set; } + + /// + /// Gets or sets a value for the time until an email expires. + /// + public TimeSpan? EmailExpiration { get; set; } } diff --git a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs index 022531c1eccf..6f373cb006c0 100644 --- a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs @@ -74,7 +74,7 @@ public override async Task SendAsync(HealthCheckResults results) var subject = _textService?.Localize("healthcheck", "scheduledHealthCheckEmailSubject", new[] { host }); EmailMessage mailMessage = CreateMailMessage(subject, message); - Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck); + Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck, false, null); if (task is not null) { await task; diff --git a/src/Umbraco.Core/Mail/IEmailSender.cs b/src/Umbraco.Core/Mail/IEmailSender.cs index 2eb8cc826358..44cd9bd862b6 100644 --- a/src/Umbraco.Core/Mail/IEmailSender.cs +++ b/src/Umbraco.Core/Mail/IEmailSender.cs @@ -7,9 +7,28 @@ namespace Umbraco.Cms.Core.Mail; /// public interface IEmailSender { + /// + /// Sends a message asynchronously. + /// + [Obsolete("Please use the overload with expires parameter. Scheduled for removal in Umbraco 18.")] Task SendAsync(EmailMessage message, string emailType); + /// + /// Sends a message asynchronously. + /// + [Obsolete("Please use the overload with expires parameter. Scheduled for removal in Umbraco 18.")] Task SendAsync(EmailMessage message, string emailType, bool enableNotification); + /// + /// Sends a message asynchronously. + /// + Task SendAsync(EmailMessage message, string emailType, bool enableNotification = false, TimeSpan? expires = null) +#pragma warning disable CS0618 // Type or member is obsolete + => SendAsync(message, emailType, enableNotification); +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// Verifies if the email sender is configured to send emails. + /// bool CanSendRequiredEmail(); } diff --git a/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs b/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs index 7d0d2b486519..21d49db76bf9 100644 --- a/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs +++ b/src/Umbraco.Core/Mail/NotImplementedEmailSender.cs @@ -12,6 +12,10 @@ public Task SendAsync(EmailMessage message, string emailType, bool enableNotific throw new NotImplementedException( "To send an Email ensure IEmailSender is implemented with a custom implementation"); + public Task SendAsync(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires) => + throw new NotImplementedException( + "To send an Email ensure IEmailSender is implemented with a custom implementation"); + public bool CanSendRequiredEmail() => throw new NotImplementedException( "To send an Email ensure IEmailSender is implemented with a custom implementation"); diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index 3d5673fd7ad1..5a210777a044 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -557,7 +557,7 @@ private void Process(BlockingCollection notificationRequest { ThreadPool.QueueUserWorkItem(state => { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug("Begin processing notifications."); } @@ -569,9 +569,9 @@ private void Process(BlockingCollection notificationRequest { try { - _emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter() + _emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification, false, null).GetAwaiter() .GetResult(); - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email); } diff --git a/src/Umbraco.Infrastructure/Mail/BasicSmtpEmailSenderClient.cs b/src/Umbraco.Infrastructure/Mail/BasicSmtpEmailSenderClient.cs index 15ae1d0f4992..03687f6be832 100644 --- a/src/Umbraco.Infrastructure/Mail/BasicSmtpEmailSenderClient.cs +++ b/src/Umbraco.Infrastructure/Mail/BasicSmtpEmailSenderClient.cs @@ -15,28 +15,44 @@ namespace Umbraco.Cms.Infrastructure.Mail public class BasicSmtpEmailSenderClient : IEmailSenderClient { private readonly GlobalSettings _globalSettings; + + /// public BasicSmtpEmailSenderClient(IOptionsMonitor globalSettings) - { - _globalSettings = globalSettings.CurrentValue; - } + => _globalSettings = globalSettings.CurrentValue; + /// public async Task SendAsync(EmailMessage message) + => await SendAsync(message, null); + + /// + public async Task SendAsync(EmailMessage message, TimeSpan? expires) { using var client = new SmtpClient(); await client.ConnectAsync( _globalSettings.Smtp!.Host, - _globalSettings.Smtp.Port, + _globalSettings.Smtp.Port, (SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) && - !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) + !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) { await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); } var mimeMessage = message.ToMimeMessage(_globalSettings.Smtp!.From); + if (_globalSettings.IsSmtpExpiryConfigured) + { + expires ??= _globalSettings.Smtp.EmailExpiration; + } + + if (expires.HasValue) + { + // `Expires` header needs to be in RFC 1123/2822 compatible format + mimeMessage.Headers.Add("Expires", DateTimeOffset.UtcNow.Add(expires.GetValueOrDefault()).ToString("R")); + } + if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) { await client.SendAsync(mimeMessage); diff --git a/src/Umbraco.Infrastructure/Mail/EmailSender.cs b/src/Umbraco.Infrastructure/Mail/EmailSender.cs index feb00735f223..fc9f71c95fab 100644 --- a/src/Umbraco.Infrastructure/Mail/EmailSender.cs +++ b/src/Umbraco.Infrastructure/Mail/EmailSender.cs @@ -30,6 +30,9 @@ public class EmailSender : IEmailSender private GlobalSettings _globalSettings; private readonly IEmailSenderClient _emailSenderClient; + /// + /// Initializes a new instance of the class. + /// [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public EmailSender( ILogger logger, @@ -39,6 +42,9 @@ public EmailSender( { } + /// + /// Initializes a new instance of the class. + /// [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public EmailSender( ILogger logger, @@ -55,6 +61,9 @@ public EmailSender( globalSettings.OnChange(x => _globalSettings = x); } + /// + /// Initializes a new instance of the class. + /// [ActivatorUtilitiesConstructor] public EmailSender( ILogger logger, @@ -72,19 +81,19 @@ public EmailSender( globalSettings.OnChange(x => _globalSettings = x); } - /// - /// Sends the message async - /// - /// + /// public async Task SendAsync(EmailMessage message, string emailType) => - await SendAsyncInternal(message, emailType, false); + await SendAsyncInternal(message, emailType, false, null); + /// public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => - await SendAsyncInternal(message, emailType, enableNotification); + await SendAsyncInternal(message, emailType, enableNotification, null); - /// - /// Returns true if the application should be able to send a required application email - /// + /// + public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification = false, TimeSpan? expires = null) => + await SendAsyncInternal(message, emailType, enableNotification, expires); + + /// /// /// We assume this is possible if either an event handler is registered or an smtp server is configured /// or a pickup directory location is configured @@ -93,7 +102,7 @@ public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured || _globalSettings.IsPickupDirectoryLocationConfigured || _notificationHandlerRegistered; - private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification) + private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires) { if (enableNotification) { @@ -104,7 +113,7 @@ private async Task SendAsyncInternal(EmailMessage message, string emailType, boo // if a handler handled sending the email then don't continue. if (notification.IsHandled) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug( "The email sending for {Subject} was handled by a notification handler", @@ -116,7 +125,7 @@ private async Task SendAsyncInternal(EmailMessage message, string emailType, boo if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured) { - if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug( "Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", @@ -173,7 +182,6 @@ private async Task SendAsyncInternal(EmailMessage message, string emailType, boo while (true); } - await _emailSenderClient.SendAsync(message); + await _emailSenderClient.SendAsync(message, expires); } - } diff --git a/src/Umbraco.Infrastructure/Mail/Interfaces/IEmailSenderClient.cs b/src/Umbraco.Infrastructure/Mail/Interfaces/IEmailSenderClient.cs index 10dd5284c40f..3749d71bed3e 100644 --- a/src/Umbraco.Infrastructure/Mail/Interfaces/IEmailSenderClient.cs +++ b/src/Umbraco.Infrastructure/Mail/Interfaces/IEmailSenderClient.cs @@ -3,15 +3,25 @@ namespace Umbraco.Cms.Infrastructure.Mail.Interfaces { /// - /// Client for sending an email from a MimeMessage + /// Client for sending an email from a MimeMessage. /// public interface IEmailSenderClient { /// - /// Sends the email message + /// Sends the email message. /// - /// - /// + /// The to send. + [Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")] public Task SendAsync(EmailMessage message); + + /// + /// Sends the email message with an expiration date. + /// + /// The to send. + /// An optional time for expiry. + public Task SendAsync(EmailMessage message, TimeSpan? expires) +#pragma warning disable CS0618 // Type or member is obsolete + => SendAsync(message); +#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/src/Umbraco.Infrastructure/Security/EmailUserForgotPasswordSender.cs b/src/Umbraco.Infrastructure/Security/EmailUserForgotPasswordSender.cs index 6c276a21bb4d..784b14af7660 100644 --- a/src/Umbraco.Infrastructure/Security/EmailUserForgotPasswordSender.cs +++ b/src/Umbraco.Infrastructure/Security/EmailUserForgotPasswordSender.cs @@ -68,7 +68,7 @@ public async Task SendForgotPassword(UserForgotPasswordMessage messageModel) var message = new EmailMessage(senderEmail, address.ToString(), emailSubject, emailBody, true); - await _emailSender.SendAsync(message, Constants.Web.EmailTypes.PasswordReset, true); + await _emailSender.SendAsync(message, Constants.Web.EmailTypes.PasswordReset, true, _securitySettings.PasswordResetEmailExpiry); } public bool CanSend() => _securitySettings.AllowPasswordReset && _emailSender.CanSendRequiredEmail(); diff --git a/src/Umbraco.Infrastructure/Security/EmailUserInviteSender.cs b/src/Umbraco.Infrastructure/Security/EmailUserInviteSender.cs index b6ef7a7447a8..6222e01ef243 100644 --- a/src/Umbraco.Infrastructure/Security/EmailUserInviteSender.cs +++ b/src/Umbraco.Infrastructure/Security/EmailUserInviteSender.cs @@ -1,9 +1,11 @@ -using System.Globalization; +using System.Globalization; using System.Net; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using MimeKit; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Email; @@ -18,15 +20,31 @@ public class EmailUserInviteSender : IUserInviteSender private readonly IEmailSender _emailSender; private readonly ILocalizedTextService _localizedTextService; private readonly GlobalSettings _globalSettings; + private readonly SecuritySettings _securitySettings; + [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")] public EmailUserInviteSender( IEmailSender emailSender, ILocalizedTextService localizedTextService, IOptions globalSettings) + : this( + emailSender, + localizedTextService, + globalSettings, + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public EmailUserInviteSender( + IEmailSender emailSender, + ILocalizedTextService localizedTextService, + IOptions globalSettings, + IOptions securitySettings) { _emailSender = emailSender; _localizedTextService = localizedTextService; _globalSettings = globalSettings.Value; + _securitySettings = securitySettings.Value; } public async Task InviteUser(UserInvitationMessage invite) @@ -67,7 +85,7 @@ public async Task InviteUser(UserInvitationMessage invite) var message = new EmailMessage(senderEmail, address.ToString(), emailSubject, emailBody, true); - await _emailSender.SendAsync(message, Constants.Web.EmailTypes.UserInvite, true); + await _emailSender.SendAsync(message, Constants.Web.EmailTypes.UserInvite, true, _securitySettings.UserInviteEmailExpiry); } public bool CanSendInvites() => _emailSender.CanSendRequiredEmail(); From a4c373d3b5cea4c72f92d8976a39401d8ab8b03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=BCger?= <93977820+OskarKruger@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:56:30 +0200 Subject: [PATCH 013/155] Entity Actions: Create button discernible text (fixes #20205) (#20434) * added hovering and focus border to RTE * fix main to OG * fix to main again * I'm going to cry * Missing localiztion feature, maybe UmbLitElement? * added localization controller to fetch localized version * localization successful for viewActionsFor and CreateFor * clean up button text * Changed label for content header to display proper name * clean up code * Included button labels for media section * clean code * Relocated localization keys, as `actions_viewActionsFor` already existed. Also made into a function, to support a fallback label. * Simplified the "Create for" label/localization Removed the need for a `getCreateAriaLabel()` method. * Removed the double-localizations (of `actions_viewActionsFor`) as the "umb-entity-actions-bundle" component handles this now. * imports tidy-up * Simplified localization key condition --------- Co-authored-by: Oskar kruger Co-authored-by: leekelleher --- .../src/assets/lang/de.ts | 2 + .../src/assets/lang/en.ts | 1 + .../picker/clipboard-entry-picker.element.ts | 2 +- .../entity-actions-bundle.element.ts | 29 ++++++------- .../common/create/create.action.kind.ts | 2 +- .../common/create/create.action.ts | 10 ++--- ...ntity-actions-table-column-view.element.ts | 2 +- .../menu-item-layout.element.ts | 42 ++++++++++--------- ...idebar-menu-with-entity-actions.element.ts | 5 ++- .../tree-item-base/tree-item-element-base.ts | 11 ++--- .../entity-actions/create/create.action.ts | 4 +- .../entity-actions/create/manifests.ts | 2 +- .../entity-actions/create/create.action.ts | 5 +-- .../media/entity-actions/create/manifests.ts | 2 +- 14 files changed, 62 insertions(+), 57 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/de.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/de.ts index c183998bfb20..071dae12ee0e 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/de.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/de.ts @@ -18,6 +18,7 @@ export default { changeDataType: 'Datentyp ändern', copy: 'Kopieren', create: 'Neu', + createFor: (name: string) => (name ? `Neu erstellen für ${name}` : 'Neu'), export: 'Exportieren', createPackage: 'Neues Paket', createGroup: 'Neue Gruppe', @@ -63,6 +64,7 @@ export default { unlock: 'Freigeben', createblueprint: 'Inhaltsvorlage anlegen', resendInvite: 'Einladung erneut versenden', + viewActionsFor: (name: string) => (name ? `Aktionen anzeigen für ${name}` : 'Aktionen anzeigen'), }, actionCategories: { content: 'Inhalt', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 5f5f6d57c058..f57a8dbea347 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -23,6 +23,7 @@ export default { copy: 'Duplicate', copyTo: 'Duplicate to', create: 'Create', + createFor: (name: string) => (name ? `Create item for ${name}` : 'Create'), createblueprint: 'Create Document Blueprint', createGroup: 'Create group', createPackage: 'Create Package', diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts index 9f67b19f5cd6..244be5167905 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts @@ -156,7 +156,7 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement { slot="actions" .entityType=${item.entityType} .unique=${item.unique} - .label=${this.localize.term('actions_viewActionsFor', [item.name])}> + .label=${this.localize.string(item.name)}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts index 33904c550de8..b4d763e392f8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts @@ -1,10 +1,10 @@ import { UmbEntityContext } from '../../entity/entity.context.js'; -import type { UmbEntityAction, ManifestEntityActionDefaultKind } from '@umbraco-cms/backoffice/entity-action'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; -import { html, nothing, customElement, property, state, ifDefined, css } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { css, customElement, html, ifDefined, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbEntityAction, ManifestEntityActionDefaultKind } from '@umbraco-cms/backoffice/entity-action'; @customElement('umb-entity-actions-bundle') export class UmbEntityActionsBundleElement extends UmbLitElement { @@ -114,14 +114,13 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { override render() { if (this._numberOfActions === 0) return nothing; - return html`${this.#renderMore()} ${this.#renderFirstAction()} `; + return html`${this.#renderMore()}${this.#renderFirstAction()}`; } #renderMore() { if (this._numberOfActions === 1) return nothing; - return html` - + `; @@ -129,13 +128,15 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { #renderFirstAction() { if (!this._firstActionApi || !this._firstActionManifest) return nothing; - return html` - - `; + return html` + + + + `; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.kind.ts index a220d955aa3b..be22c22bec03 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.kind.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.kind.ts @@ -15,7 +15,7 @@ export const manifest: UmbExtensionManifestKind = { forEntityTypes: [], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.ts index 6b85c7ec3598..322a5ceef06b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.ts @@ -2,19 +2,15 @@ import { UmbEntityActionBase } from '../../entity-action-base.js'; import type { UmbEntityActionArgs } from '../../types.js'; import type { MetaEntityActionCreateKind } from './types.js'; import { UMB_ENTITY_CREATE_OPTION_ACTION_LIST_MODAL } from './modal/constants.js'; +import { createExtensionApi, UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { - type UmbExtensionManifestInitializer, - createExtensionApi, - UmbExtensionsManifestInitializer, - type PermittedControllerType, -} from '@umbraco-cms/backoffice/extension-api'; import type { ManifestEntityCreateOptionAction, UmbEntityCreateOptionAction, } from '@umbraco-cms/backoffice/entity-create-option-action'; +import type { PermittedControllerType, UmbExtensionManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbCreateEntityAction extends UmbEntityActionBase { #hasSingleOption = true; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts index d2ca007786c1..925ccd4ca98d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/global-components/entity-actions-table-column-view/entity-actions-table-column-view.element.ts @@ -14,7 +14,7 @@ export class UmbEntityActionsTableColumnViewElement extends UmbLitElement { + .label=${this.localize.string((this.value as any).name)}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/components/menu-item-layout/menu-item-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/components/menu-item-layout/menu-item-layout.element.ts index fdc2c07b7abf..b957066e6fde 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/components/menu-item-layout/menu-item-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/components/menu-item-layout/menu-item-layout.element.ts @@ -1,4 +1,4 @@ -import { html, customElement, property, ifDefined, state } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, ifDefined, property, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { ensureSlash } from '@umbraco-cms/backoffice/router'; import { debounce } from '@umbraco-cms/backoffice/utils'; @@ -72,24 +72,28 @@ export class UmbMenuItemLayoutElement extends UmbLitElement { } override render() { - return html` - - ${this.entityType - ? html` - ` - : ''} - - `; + return html` + + + ${when( + this.entityType, + () => html` + + `, + )} + + + `; } override disconnectedCallback() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu-with-entity-actions/section-sidebar-menu-with-entity-actions.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu-with-entity-actions/section-sidebar-menu-with-entity-actions.element.ts index f7c885fc10e1..4dbcb61fb151 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu-with-entity-actions/section-sidebar-menu-with-entity-actions.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/section-sidebar-menu-with-entity-actions/section-sidebar-menu-with-entity-actions.element.ts @@ -21,14 +21,15 @@ export class UmbSectionSidebarMenuWithEntityActionsElement extends UmbSectionSid } override renderHeader() { + const label = this.localize.string(this.manifest?.meta?.label ?? ''); return html` `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts index 0b0d1764a80d..2932c22a3c97 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts @@ -138,6 +138,9 @@ export abstract class UmbTreeItemElementBase< // Note: Currently we want to prevent opening when the item is in a selectable context, but this might change in the future. // If we like to be able to open items in selectable context, then we might want to make it as a menu item action, so you have to click ... and chose an action called 'Edit' override render() { + const caretLabelKey = this._isOpen + ? 'visuallyHiddenTexts_collapseChildItems' + : 'visuallyHiddenTexts_expandChildItems'; return html` + href=${ifDefined(this._isSelectableContext ? undefined : this._href)}> ${this.renderIconContainer()} ${this.renderLabel()} ${this.#renderActions()} ${this.#renderChildItems()} ${this.#renderPaging()} @@ -224,7 +225,7 @@ export abstract class UmbTreeItemElementBase< slot="actions" .entityType=${this.#api.entityType} .unique=${this.#api.unique} - .label=${this.localize.term('actions_viewActionsFor', [this._label])}> + .label=${this._label}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/create.action.ts index 9383940f682b..9f6f4898cad8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/create.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/create.action.ts @@ -1,9 +1,9 @@ import { UmbDocumentItemRepository } from '../../item/index.js'; import { UMB_DOCUMENT_CREATE_OPTIONS_MODAL } from './document-create-options-modal.token.js'; -import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action'; +import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action'; export class UmbCreateDocumentEntityAction extends UmbEntityActionBase { constructor(host: UmbControllerHost, args: UmbEntityActionArgs) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts index 1cdd48b96d9b..361f62a30a43 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts @@ -13,7 +13,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_DOCUMENT_ROOT_ENTITY_TYPE, UMB_DOCUMENT_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, conditions: [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/create.action.ts index 3629d1424ba8..b8b65c17ff6a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/create.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/create.action.ts @@ -1,9 +1,9 @@ import { UmbMediaItemRepository } from '../../repository/index.js'; import { UMB_MEDIA_CREATE_OPTIONS_MODAL } from './media-create-options-modal.token.js'; -import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action'; +import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action'; export class UmbCreateMediaEntityAction extends UmbEntityActionBase { constructor(host: UmbControllerHost, args: UmbEntityActionArgs) { @@ -30,5 +30,4 @@ export class UmbCreateMediaEntityAction extends UmbEntityActionBase { }); } } - export { UmbCreateMediaEntityAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/manifests.ts index 7572bca3ebb6..a2261aeb9ac8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/manifests.ts @@ -12,7 +12,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_MEDIA_ROOT_ENTITY_TYPE, UMB_MEDIA_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, conditions: [ From de449079ddfe90f54478bfae0ac23dd28d9ac752 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Fri, 10 Oct 2025 08:43:32 +0100 Subject: [PATCH 014/155] Fixed build error --- .../clipboard-entry/picker/clipboard-entry-picker.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts index 244be5167905..fda340058b6c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts @@ -156,7 +156,7 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement { slot="actions" .entityType=${item.entityType} .unique=${item.unique} - .label=${this.localize.string(item.name)}> + .label=${this.localize.string(item.name ?? '')}> `; } From 99c2aaf17abc3d1b13b3843503f449b7a1ae82a7 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 10 Oct 2025 09:52:57 +0200 Subject: [PATCH 015/155] Members: Forward port of fix for member lockout issue #16988 from PR #17007 for 16 (#20441) * Port PR #17007 * Update src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Security/IdentityMapDefinition.cs | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 5032a2a87182..c3046342908e 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -95,7 +95,7 @@ private void Map(IUser source, BackOfficeIdentityUser target) target.IsApproved = source.IsApproved; target.SecurityStamp = source.SecurityStamp; DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes); - target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null; + target.LockoutEnd = source.IsLockedOut ? lockedOutUntil ?? DateTime.MaxValue : null; target.Kind = source.Kind; } @@ -114,16 +114,46 @@ private void Map(IMember source, MemberIdentityUser target) target.IsApproved = source.IsApproved; target.SecurityStamp = source.SecurityStamp; DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes); - target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null; + target.LockoutEnd = GetLockoutEnd(source); + target.LastLockoutDateUtc = GetLastLockoutDateUtc(source); + target.CreatedDateUtc = EnsureUtcWithServerTime(source.CreateDate); target.Comments = source.Comments; - target.LastLockoutDateUtc = source.LastLockoutDate == DateTime.MinValue - ? null - : source.LastLockoutDate?.ToUniversalTime(); - target.CreatedDateUtc = source.CreateDate.ToUniversalTime(); target.Key = source.Key; target.MemberTypeAlias = source.ContentTypeAlias; target.TwoFactorEnabled = _twoFactorLoginService.IsTwoFactorEnabledAsync(source.Key).GetAwaiter().GetResult(); // NB: same comments re AutoMapper as per BackOfficeUser } + + private DateTimeOffset? GetLockoutEnd(IMember source) + { + if (source.IsLockedOut is false) + { + return null; + } + + DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes); + if (lockedOutUntil.HasValue is false) + { + return DateTime.MaxValue; + } + + return EnsureUtcWithServerTime(lockedOutUntil.Value); + } + + private static DateTime? GetLastLockoutDateUtc(IMember source) + { + if (source.LastLockoutDate is null || source.LastLockoutDate == DateTime.MinValue) + { + return null; + } + + return EnsureUtcWithServerTime(source.LastLockoutDate.Value); + } + + private static DateTime EnsureUtcWithServerTime(DateTime date) => + + // We have a server time value here, but the Kind is UTC, so we can't use .ToUniversalTime() to convert to the UTC + // value that the LockoutEnd property expects. We need to create a DateTimeOffset with the correct offset. + DateTime.SpecifyKind(date, DateTimeKind.Local).ToUniversalTime(); } From fd34ce5bd7ec9af2fe10d05adb4a47747fb1aee9 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Fri, 10 Oct 2025 10:32:19 +0200 Subject: [PATCH 016/155] Icon Picker: Fit icons scroll container to modal height (#20438) * Fill height and align icons to top * Auto scrollbar instead * Auto height of grid rows * Enforce scroll again --- .../icon-picker-modal/icon-picker-modal.element.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts index e6aa5a8bfd9b..0311318845ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts @@ -190,7 +190,10 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement Date: Fri, 10 Oct 2025 15:00:47 +0200 Subject: [PATCH 017/155] Refactoring: Fixed spelling mistake in method name (#20460) * Fixed spelling mistake in method name. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Kenn Jacobsen Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Controllers/RenderController.cs | 2 +- .../Controllers/UmbracoPageController.cs | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs index d1899b5db4d6..2d1d89f47f63 100644 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -112,7 +112,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context /// protected override IActionResult CurrentTemplate(T model) { - if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) + if (EnsurePhysicalViewExists(UmbracoRouteValues.TemplateName) == false) { // no physical template file was found return new PublishedContentNotFoundResult(UmbracoContext); diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs index 4634f4e48add..64cfe3c4ccfa 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs @@ -79,7 +79,7 @@ protected virtual IPublishedContent? CurrentPage /// protected virtual IActionResult CurrentTemplate(T model) { - if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) + if (EnsurePhysicalViewExists(UmbracoRouteValues.TemplateName) == false) { throw new InvalidOperationException("No physical template file was found for template " + UmbracoRouteValues.TemplateName); @@ -92,7 +92,14 @@ protected virtual IActionResult CurrentTemplate(T model) /// Ensures that a physical view file exists on disk. /// /// The view name. - protected bool EnsurePhsyicalViewExists(string? template) + [Obsolete("Please use the correctly spelt EnsurePhysicalViewExists method. Scheduled for removal in Umbraco 18.")] + protected bool EnsurePhsyicalViewExists(string? template) => EnsurePhysicalViewExists(template); + + /// + /// Ensures that a physical view file exists on disk. + /// + /// The view name. + protected bool EnsurePhysicalViewExists(string? template) { if (string.IsNullOrWhiteSpace(template)) { From 7b4684cd70d03986754ef3c08cb33dc735b12042 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 13 Oct 2025 06:34:02 +0200 Subject: [PATCH 018/155] UX: Center align log type in media history view (#20469) Center align log type in media --- .../info-app/media-history-workspace-info-app.element.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/media-history-workspace-info-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/media-history-workspace-info-app.element.ts index a728e44c10ff..80955b2f2bf4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/media-history-workspace-info-app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/media-history-workspace-info-app.element.ts @@ -173,6 +173,7 @@ export class UmbMediaHistoryWorkspaceInfoAppElement extends UmbLitElement { } .log-type uui-tag { + justify-self: center; height: fit-content; margin-top: auto; margin-bottom: auto; From 0a027dd80d8e66d4ddfe3d176b86b3219fb57ed7 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:12:45 +0200 Subject: [PATCH 019/155] Dependencies: Fixed dependency conflicts when installing Microsoft.EntityFrameworkCore.Design (closes #20421) (#20474) * Add explicit references to Microsoft.CodeAnalysis.* packages to fix conflicts when installing Microsoft.EntityFrameworkCore.Design This allows consumers to simply install Microsoft.EntityFrameworkCore.Design without having to manually install specific versions to deal with transitive dependency problems. * Disable CPM for UI project to better reflect consumers * Update src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Directory.Packages.props | 3 --- .../Umbraco.Infrastructure.csproj | 6 +++++- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 15 +++------------ 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 94a3dd9c676d..ca3619254235 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,14 +14,11 @@ - - - diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 7df82c5cb032..1ae0863fe142 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -64,8 +64,12 @@ - + + + + + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index d3a32481d540..a2fa5d39eaf7 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -5,6 +5,7 @@ false false + false @@ -23,23 +24,13 @@ - - - - - - - - - - - + - + From 3ac37f3686f529edfe918a845f4b0c35b897c293 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Mon, 13 Oct 2025 16:32:48 +0100 Subject: [PATCH 020/155] Recycle Bin: Trigger cache invalidation for trashed document/media items (#20483) * Configure document/media items to listen for `Trashed` server-events for cache invalidation * Fire reload event on restore destination tree/menu * Removed "trashed" part of the code comment --- .../restore-from-recycle-bin-modal.element.ts | 2 +- .../restore-from-recycle-bin.action.ts | 19 ++++++++++++++----- ...-item.server.cache-invalidation.manager.ts | 3 ++- .../detail/cache-invalidation.manager.ts | 5 ++++- .../item/cache-invalidation.manager.ts | 5 ++++- ...-item.server.cache-invalidation.manager.ts | 3 ++- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.element.ts index 8370cc78b0dd..77dfcdcd2ced 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.element.ts @@ -53,7 +53,7 @@ export class UmbRestoreFromRecycleBinModalElement extends UmbModalBaseElement< this.#setDestinationValue({ unique: null, - entityType: 'unknown', + entityType: this.data?.entityType ?? 'unknown', }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/restore-from-recycle-bin.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/restore-from-recycle-bin.action.ts index cd8e4ac1b2fc..af7d3e410c5b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/restore-from-recycle-bin.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/restore-from-recycle-bin.action.ts @@ -1,7 +1,11 @@ import { UMB_RESTORE_FROM_RECYCLE_BIN_MODAL } from './modal/restore-from-recycle-bin-modal.token.js'; import type { MetaEntityActionRestoreFromRecycleBinKind } from './types.js'; import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; -import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action'; +import { + UmbEntityActionBase, + UmbRequestReloadChildrenOfEntityEvent, + UmbRequestReloadStructureForEntityEvent, +} from '@umbraco-cms/backoffice/entity-action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; /** @@ -33,15 +37,20 @@ export class UmbRestoreFromRecycleBinEntityAction extends UmbEntityActionBase { dataCache: UmbManagementApiDetailDataCache; eventSources: Array; + eventTypes?: Array; } export class UmbManagementApiDetailDataCacheInvalidationManager extends UmbControllerBase { protected _dataCache: UmbManagementApiDetailDataCache; #eventSources: Array; + #eventTypes: Array; #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE; constructor( @@ -22,6 +24,7 @@ export class UmbManagementApiDetailDataCacheInvalidationManager { this.#serverEventContext = context; @@ -42,7 +45,7 @@ export class UmbManagementApiDetailDataCacheInvalidationManager { if (!event) return; this._onServerEvent(event); diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache-invalidation.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache-invalidation.manager.ts index ee6a275f7887..8c8d397c7547 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache-invalidation.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache-invalidation.manager.ts @@ -7,11 +7,13 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; export interface UmbManagementApiItemDataInvalidationManagerArgs { dataCache: UmbManagementApiItemDataCache; eventSources: Array; + eventTypes?: Array; } export class UmbManagementApiItemDataCacheInvalidationManager extends UmbControllerBase { protected _dataCache: UmbManagementApiItemDataCache; #eventSources: Array; + #eventTypes: Array; #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE; constructor(host: UmbControllerHost, args: UmbManagementApiItemDataInvalidationManagerArgs) { @@ -19,6 +21,7 @@ export class UmbManagementApiItemDataCacheInvalidationManager { this.#serverEventContext = context; @@ -40,7 +43,7 @@ export class UmbManagementApiItemDataCacheInvalidationManager { if (!event) return; this._onServerEvent(event); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.cache-invalidation.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.cache-invalidation.manager.ts index fb6933c10a25..75a95ff9b2b2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.cache-invalidation.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.cache-invalidation.manager.ts @@ -10,9 +10,10 @@ export class UmbManagementApiMediaItemDataCacheInvalidationManager extends UmbMa constructor(host: UmbControllerHost) { super(host, { dataCache: mediaItemCache, - /* The Media item model includes info about the Media Type. + /* The Media item model includes info about the Media Type. We need to invalidate the cache for both Media and MediaType events. */ eventSources: ['Umbraco:CMS:Media', 'Umbraco:CMS:MediaType'], + eventTypes: ['Updated', 'Deleted', 'Trashed'], }); } From 28e4caaad37acab1df11c3180059bed7bfb191de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 13 Oct 2025 18:42:06 +0200 Subject: [PATCH 021/155] fix merge gone wrong --- .../tree/tree-item/tree-item-base/tree-item-element-base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts index edb8d18817be..296409029889 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts @@ -213,7 +213,7 @@ export abstract class UmbTreeItemElementBase< .showChildren=${this._isOpen} .caretLabel=${this.localize.term(caretLabelKey) + ' ' + this._label} label=${ifDefined(this._label)} - href=${ifDefined(this._isSelectableContext ? undefined : this._href)}> + href=${ifDefined(this._isSelectableContext ? undefined : this._href)} .renderExpandSymbol=${this._renderExpandSymbol}> ${this.#renderLoadPrevButton()} ${this.renderIconContainer()} ${this.renderLabel()} ${this.#renderActions()} ${this.#renderChildItems()} From 3393febdcac6a648dcfc8758d981c71fc157f031 Mon Sep 17 00:00:00 2001 From: Nicklas Kramer Date: Mon, 13 Oct 2025 20:20:01 +0200 Subject: [PATCH 022/155] News dashboard: API and rendering of news stories on dashboard (#20416) * Adding controller * Lower case route to match other endpoints * Adding service and typed output * Renaming to NewsDashboard * Moving more stuff to service * Removing unused code * Some refactoring in accordance with better architecture * Created repository and mock data source for the news dashboard also display some data in the UI. * Minor refactoring: naming, aligning with existing controller patterns. * Update OpenApi.json. * Update typed client sdk and types. * Provide language to API endpoint, just in case we want to localize news in the future. * Obsoleted configuration * Moved mock data to mocks folder and updated repository to use the actual response model and service from the Api * Prepared news repository with server data source. * Rendered news items according to required group structure. Added TODOs for remaining tasks. * Fixed FE build issues. * Update src/Umbraco.Core/Constants-Configuration.cs * Fixed grid spacing, sanitize code and make the styles closer to the v13. * Added container query and padding to the card body. * Fix padding * Fixed title according to priority. * Relocated/renamed the news server data-source file * Simplified the news repo/data-source classes by extending `UmbControllerBase`, the host constructor is handled for us. * Added `types.ts` export type files * Refactored interface name + typing * Added `uui-loader` component * Tweaked styles, added box-shadow to cards Added flexbox gap to the card body. * Sorted import order --------- Co-authored-by: Andy Butland Co-authored-by: engjlr Co-authored-by: leekelleher --- .../NewsDashboard/NewsDashboardController.cs | 22 + .../NewsDashboardControllerBase.cs | 10 + .../DashboardBuilderExtensions.cs | 15 + .../UmbracoBuilderExtensions.cs | 3 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 589 +++++++++++++++++- .../NewsDashboard/INewsDashboardService.cs | 18 + .../NewsDashboard/NewsDashboardService.cs | 102 +++ .../NewsDashboardItemResponseModel.cs | 18 + .../NewsDashboardResponseModel.cs | 6 + src/Umbraco.Core/Constants-Configuration.cs | 1 + .../src/assets/lang/en.ts | 2 +- .../src/mocks/browser-handlers.ts | 2 + .../src/mocks/data/umbraco-news.data.ts | 115 ++++ .../mocks/handlers/umbraco-news.handlers.ts | 10 + .../src/packages/core/backend-api/sdk.gen.ts | 77 ++- .../packages/core/backend-api/types.gen.ts | 198 +++++- .../src/packages/umbraco-news/index.ts | 3 + .../packages/umbraco-news/repository/index.ts | 2 + .../packages/umbraco-news/repository/types.ts | 1 + .../repository/umbraco-news.respository.ts | 12 + .../umbraco-news.server.data-source.ts | 25 + .../src/packages/umbraco-news/types.ts | 1 + .../umbraco-news-dashboard.element.ts | 308 +++++++-- 23 files changed, 1487 insertions(+), 53 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/NewsDashboard/NewsDashboardController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/NewsDashboard/NewsDashboardControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/DependencyInjection/DashboardBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/NewsDashboard/INewsDashboardService.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/NewsDashboard/NewsDashboardService.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/NewsDashboard/NewsDashboardItemResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/NewsDashboard/NewsDashboardResponseModel.cs create mode 100644 src/Umbraco.Web.UI.Client/src/mocks/data/umbraco-news.data.ts create mode 100644 src/Umbraco.Web.UI.Client/src/mocks/handlers/umbraco-news.handlers.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/umbraco-news/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/umbraco-news.respository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/umbraco-news.server.data-source.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/umbraco-news/types.ts diff --git a/src/Umbraco.Cms.Api.Management/Controllers/NewsDashboard/NewsDashboardController.cs b/src/Umbraco.Cms.Api.Management/Controllers/NewsDashboard/NewsDashboardController.cs new file mode 100644 index 000000000000..583a8323b45d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/NewsDashboard/NewsDashboardController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Services.NewsDashboard; +using Umbraco.Cms.Api.Management.ViewModels.NewsDashboard; + +namespace Umbraco.Cms.Api.Management.Controllers.NewsDashboard; + +public class NewsDashboardController : NewsDashboardControllerBase +{ + private readonly INewsDashboardService _newsDashboardService; + + public NewsDashboardController(INewsDashboardService newsDashboardService) => _newsDashboardService = newsDashboardService; + + [HttpGet] + [ProducesResponseType(typeof(NewsDashboardResponseModel), StatusCodes.Status200OK)] + public async Task GetDashboard() + { + NewsDashboardResponseModel content = await _newsDashboardService.GetItemsAsync(); + + return Ok(content); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/NewsDashboard/NewsDashboardControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/NewsDashboard/NewsDashboardControllerBase.cs new file mode 100644 index 000000000000..a5d8eb8eb5fd --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/NewsDashboard/NewsDashboardControllerBase.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; + +namespace Umbraco.Cms.Api.Management.Controllers.NewsDashboard; + +[VersionedApiBackOfficeRoute("news-dashboard")] +[ApiExplorerSettings(GroupName = "News Dashboard")] +public abstract class NewsDashboardControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/DashboardBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/DashboardBuilderExtensions.cs new file mode 100644 index 000000000000..914546a6d0b3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/DashboardBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.NewsDashboard; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class NewsDashboardBuilderExtensions +{ + internal static IUmbracoBuilder AddNewsDashboard(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index a788b1fad24d..a99e6de55303 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -71,7 +71,8 @@ public static IUmbracoBuilder AddUmbracoManagementApi(this IUmbracoBuilder build .AddUserData() .AddSegment() .AddExport() - .AddImport(); + .AddImport() + .AddNewsDashboard(); services .ConfigureOptions() diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 64674eb86f71..5f13509f568b 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -20321,6 +20321,461 @@ ] } }, + "/umbraco/management/api/v1/member-type/folder": { + "post": { + "tags": [ + "Member Type" + ], + "operationId": "PostMemberTypeFolder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateFolderRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateFolderRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateFolderRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + }, + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice-User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/member-type/folder/{id}": { + "get": { + "tags": [ + "Member Type" + ], + "operationId": "GetMemberTypeFolderById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FolderResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" + } + }, + "security": [ + { + "Backoffice-User": [ ] + } + ] + }, + "delete": { + "tags": [ + "Member Type" + ], + "operationId": "DeleteMemberTypeFolderById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice-User": [ ] + } + ] + }, + "put": { + "tags": [ + "Member Type" + ], + "operationId": "PutMemberTypeFolderById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateFolderResponseModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateFolderResponseModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateFolderResponseModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, + "security": [ + { + "Backoffice-User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/tree/member-type/root": { "get": { "tags": [ @@ -21778,6 +22233,38 @@ ] } }, + "/umbraco/management/api/v1/news-dashboard": { + "get": { + "tags": [ + "News Dashboard" + ], + "operationId": "GetNewsDashboard", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/NewsDashboardResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice-User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/object-types": { "get": { "tags": [ @@ -37354,6 +37841,9 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -37640,6 +38130,9 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -38405,7 +38898,8 @@ "type": "boolean" }, "allowNonExistingSegmentsCreation": { - "type": "boolean" + "type": "boolean", + "deprecated": true } }, "additionalProperties": false @@ -39037,6 +39531,36 @@ }, "additionalProperties": false }, + "DocumentTypePermissionPresentationModel": { + "required": [ + "$type", + "documentTypeAlias", + "verbs" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "verbs": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "documentTypeAlias": { + "type": "string" + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DocumentTypePermissionPresentationModel": "#/components/schemas/DocumentTypePermissionPresentationModel" + } + } + }, "DocumentTypePropertyTypeContainerResponseModel": { "required": [ "id", @@ -42732,6 +43256,61 @@ }, "additionalProperties": false }, + "NewsDashboardItemResponseModel": { + "required": [ + "header", + "priority" + ], + "type": "object", + "properties": { + "priority": { + "type": "string" + }, + "header": { + "type": "string" + }, + "body": { + "type": "string", + "nullable": true + }, + "buttonText": { + "type": "string", + "nullable": true + }, + "imageUrl": { + "type": "string", + "nullable": true + }, + "imageAltText": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "NewsDashboardResponseModel": { + "required": [ + "items" + ], + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/NewsDashboardItemResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, "NoopSetupTwoFactorModel": { "type": "object", "additionalProperties": false @@ -47740,6 +48319,9 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -48177,6 +48759,9 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -48859,4 +49444,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/INewsDashboardService.cs b/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/INewsDashboardService.cs new file mode 100644 index 000000000000..3f2b2c8b3f78 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/INewsDashboardService.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Api.Management.ViewModels.NewsDashboard; + +namespace Umbraco.Cms.Api.Management.Services.NewsDashboard; + +/// +/// Defines a service for retrieving news dashboard items. +/// +public interface INewsDashboardService +{ + /// + /// Asynchronously retrieves the collection of news dashboard items. + /// + /// + /// A task that represents the asynchronous operation. + /// The task result contains a with the retrieved news dashboard items. + /// + Task GetItemsAsync(); +} diff --git a/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/NewsDashboardService.cs b/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/NewsDashboardService.cs new file mode 100644 index 000000000000..2d05e20df374 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/NewsDashboardService.cs @@ -0,0 +1,102 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.ViewModels.NewsDashboard; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Telemetry; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Services.NewsDashboard; + +/// +public class NewsDashboardService : INewsDashboardService +{ + private readonly AppCaches _appCaches; + private readonly IUmbracoVersion _umbracoVersion; + private readonly ISiteIdentifierService _siteIdentifierService; + private readonly ILogger _logger; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly GlobalSettings _globalSettings; + + private static readonly HttpClient _httpClient = new(); + + /// + /// Initializes a new instance of the class. + /// + public NewsDashboardService( + AppCaches appCaches, + IUmbracoVersion umbracoVersion, + ISiteIdentifierService siteIdentifierService, + ILogger logger, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IOptions globalSettings) + { + _appCaches = appCaches; + _umbracoVersion = umbracoVersion; + _siteIdentifierService = siteIdentifierService; + _logger = logger; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _globalSettings = globalSettings.Value; + } + + /// + public async Task GetItemsAsync() + { + const string BaseUrl = "https://umbraco-dashboard-news.euwest01.umbraco.io"; + const string Path = "/api/News"; + + var version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); + _siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid siteIdentifier); + + var language = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Language ?? _globalSettings.DefaultUILanguage; + + var url = $"{BaseUrl}/{Path}?version={version}&siteId={siteIdentifier}&language={language}"; + + const string CacheKey = "umbraco-dashboard-news"; + NewsDashboardResponseModel? content = _appCaches.RuntimeCache.GetCacheItem(CacheKey); + if (content is not null) + { + return content; + } + + try + { + var json = await _httpClient.GetStringAsync(url); + + if (TryMapModel(json, out NewsDashboardResponseModel? model)) + { + _appCaches.RuntimeCache.InsertCacheItem(CacheKey, () => model, new TimeSpan(0, 30, 0)); + content = model; + } + } + catch (Exception ex) + { + _logger.LogError(ex.InnerException ?? ex, "Error getting dashboard content from {Url}", url); + } + + return content ?? new NewsDashboardResponseModel { Items = [] }; + } + + private bool TryMapModel(string json, [MaybeNullWhen(false)] out NewsDashboardResponseModel newsDashboardResponseModel) + { + try + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }; + + List? items = JsonSerializer.Deserialize>(json, options); + newsDashboardResponseModel = new NewsDashboardResponseModel { Items = items ?? [] }; + + return true; + } + catch (JsonException ex) + { + _logger.LogError(ex.InnerException ?? ex, "Error deserializing dashboard news items"); + newsDashboardResponseModel = null; + return false; + } + } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/NewsDashboard/NewsDashboardItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/NewsDashboard/NewsDashboardItemResponseModel.cs new file mode 100644 index 000000000000..ddad85617286 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/NewsDashboard/NewsDashboardItemResponseModel.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.NewsDashboard; + +public class NewsDashboardItemResponseModel +{ + public required string Priority { get; set; } + + public required string Header { get; set; } + + public string? Body { get; set; } + + public string? ButtonText { get; set; } + + public string? ImageUrl { get; set; } + + public string? ImageAltText { get; set; } + + public string? Url { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/NewsDashboard/NewsDashboardResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/NewsDashboard/NewsDashboardResponseModel.cs new file mode 100644 index 000000000000..e9251f03ffa1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/NewsDashboard/NewsDashboardResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.NewsDashboard; + +public class NewsDashboardResponseModel +{ + public required IEnumerable Items { get; set; } +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 57b0d543a67a..8ac24c71f45d 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -57,6 +57,7 @@ public static class Configuration public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; + [Obsolete("No longer used in Umbraco. Scheduled to be removed in Umbraco 19.")] public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 364469d9f41f..70a0ad0020dd 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2357,7 +2357,7 @@ export default { noDependency: 'No', }, dashboardTabs: { - contentIntro: 'Getting Started', + contentIntro: 'Welcome to Umbraco', contentRedirectManager: 'Redirect URL Management', mediaFolderBrowser: 'Content', settingsWelcome: 'Welcome', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts index 6003f11ff521..495a91d0c212 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts @@ -32,6 +32,7 @@ import { handlers as stylesheetHandlers } from './handlers/stylesheet/index.js'; import { handlers as tagHandlers } from './handlers/tag-handlers.js'; import { handlers as telemetryHandlers } from './handlers/telemetry.handlers.js'; import { handlers as templateHandlers } from './handlers/template/index.js'; +import { handlers as umbracoNewsHandlers } from './handlers/umbraco-news.handlers.js'; import { handlers as upgradeHandlers } from './handlers/upgrade.handlers.js'; import { handlers as userGroupsHandlers } from './handlers/user-group/index.js'; import { handlers as userHandlers } from './handlers/user/index.js'; @@ -76,6 +77,7 @@ const handlers = [ ...tagHandlers, ...telemetryHandlers, ...templateHandlers, + ...umbracoNewsHandlers, ...upgradeHandlers, ...userGroupsHandlers, ...userHandlers, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/umbraco-news.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/umbraco-news.data.ts new file mode 100644 index 000000000000..366b0b883c71 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/umbraco-news.data.ts @@ -0,0 +1,115 @@ +import type { NewsDashboardResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +export const umbNewsData: NewsDashboardResponseModel = { + items: [ + { + priority: 'High', + header: 'News High 1', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'Button text', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/rxphk1dd/dashboard-11.jpg', + imageAltText: 'Test', + url: 'https://umbraco.com', + }, + { + priority: 'High', + header: 'News high 2', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/ezujjyhj/dashboard-12.jpg', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'High', + header: 'News high 3', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/ezujjyhj/dashboard-12.jpg', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'High', + header: 'News high 4', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/ezujjyhj/dashboard-12.jpg', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'High', + header: 'News high 5', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/ezujjyhj/dashboard-12.jpg', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'Medium', + header: 'News medium 1', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: '', + imageAltText: '', + url: 'https://umbraco.com', + }, + + { + priority: 'Normal', + header: 'Normal 1', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/403pgsxa/stability-without-complexity.webp', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'Normal', + header: 'Normal 2', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/403pgsxa/stability-without-complexity.webp', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'Normal', + header: 'Normal 3', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/403pgsxa/stability-without-complexity.webp', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'Normal', + header: 'Normal 4', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/403pgsxa/stability-without-complexity.webp', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'Normal', + header: 'Normal 5', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/403pgsxa/stability-without-complexity.webp', + imageAltText: '', + url: 'https://umbraco.com', + }, + { + priority: 'Normal', + header: 'Normal 6', + body: '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed urna dui, convallis in porta sed, tempor non mauris. Aliquam placerat sed nulla a varius. Maecenas elementum tempus luctus. Nulla eu magna turpis. Nunc vel sodales urna. Donec ac ipsum sed ex egestas interdum. Aliquam pellentesque tellus id felis tempor gravida.

', + buttonText: 'See more', + imageUrl: 'https://umbraco-dashboard-news.euwest01.umbraco.io/media/403pgsxa/stability-without-complexity.webp', + imageAltText: '', + url: 'https://umbraco.com', + }, + ], +}; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/umbraco-news.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/umbraco-news.handlers.ts new file mode 100644 index 000000000000..748bed633427 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/umbraco-news.handlers.ts @@ -0,0 +1,10 @@ +const { rest } = window.MockServiceWorker; +import { umbNewsData } from '../data/umbraco-news.data.js'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const handlers = [ + rest.get(umbracoPath('/news-dashboard'), (req, res, ctx) => { + const response = umbNewsData; + return res(ctx.delay(), ctx.status(200), ctx.json(response)); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts index 7a718c555cd6..65dfc31492b9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts @@ -2,7 +2,7 @@ import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; import { client } from './client.gen'; -import type { DeleteDataTypeByIdData, DeleteDataTypeByIdErrors, DeleteDataTypeByIdResponses, DeleteDataTypeFolderByIdData, DeleteDataTypeFolderByIdErrors, DeleteDataTypeFolderByIdResponses, DeleteDictionaryByIdData, DeleteDictionaryByIdErrors, DeleteDictionaryByIdResponses, DeleteDocumentBlueprintByIdData, DeleteDocumentBlueprintByIdErrors, DeleteDocumentBlueprintByIdResponses, DeleteDocumentBlueprintFolderByIdData, DeleteDocumentBlueprintFolderByIdErrors, DeleteDocumentBlueprintFolderByIdResponses, DeleteDocumentByIdData, DeleteDocumentByIdErrors, DeleteDocumentByIdPublicAccessData, DeleteDocumentByIdPublicAccessErrors, DeleteDocumentByIdPublicAccessResponses, DeleteDocumentByIdResponses, DeleteDocumentTypeByIdData, DeleteDocumentTypeByIdErrors, DeleteDocumentTypeByIdResponses, DeleteDocumentTypeFolderByIdData, DeleteDocumentTypeFolderByIdErrors, DeleteDocumentTypeFolderByIdResponses, DeleteLanguageByIsoCodeData, DeleteLanguageByIsoCodeErrors, DeleteLanguageByIsoCodeResponses, DeleteLogViewerSavedSearchByNameData, DeleteLogViewerSavedSearchByNameErrors, DeleteLogViewerSavedSearchByNameResponses, DeleteMediaByIdData, DeleteMediaByIdErrors, DeleteMediaByIdResponses, DeleteMediaTypeByIdData, DeleteMediaTypeByIdErrors, DeleteMediaTypeByIdResponses, DeleteMediaTypeFolderByIdData, DeleteMediaTypeFolderByIdErrors, DeleteMediaTypeFolderByIdResponses, DeleteMemberByIdData, DeleteMemberByIdErrors, DeleteMemberByIdResponses, DeleteMemberGroupByIdData, DeleteMemberGroupByIdErrors, DeleteMemberGroupByIdResponses, DeleteMemberTypeByIdData, DeleteMemberTypeByIdErrors, DeleteMemberTypeByIdResponses, DeletePackageCreatedByIdData, DeletePackageCreatedByIdErrors, DeletePackageCreatedByIdResponses, DeletePartialViewByPathData, DeletePartialViewByPathErrors, DeletePartialViewByPathResponses, DeletePartialViewFolderByPathData, DeletePartialViewFolderByPathErrors, DeletePartialViewFolderByPathResponses, DeletePreviewData, DeletePreviewResponses, DeleteRecycleBinDocumentByIdData, DeleteRecycleBinDocumentByIdErrors, DeleteRecycleBinDocumentByIdResponses, DeleteRecycleBinDocumentData, DeleteRecycleBinDocumentErrors, DeleteRecycleBinDocumentResponses, DeleteRecycleBinMediaByIdData, DeleteRecycleBinMediaByIdErrors, DeleteRecycleBinMediaByIdResponses, DeleteRecycleBinMediaData, DeleteRecycleBinMediaErrors, DeleteRecycleBinMediaResponses, DeleteRedirectManagementByIdData, DeleteRedirectManagementByIdErrors, DeleteRedirectManagementByIdResponses, DeleteScriptByPathData, DeleteScriptByPathErrors, DeleteScriptByPathResponses, DeleteScriptFolderByPathData, DeleteScriptFolderByPathErrors, DeleteScriptFolderByPathResponses, DeleteStylesheetByPathData, DeleteStylesheetByPathErrors, DeleteStylesheetByPathResponses, DeleteStylesheetFolderByPathData, DeleteStylesheetFolderByPathErrors, DeleteStylesheetFolderByPathResponses, DeleteTemplateByIdData, DeleteTemplateByIdErrors, DeleteTemplateByIdResponses, DeleteTemporaryFileByIdData, DeleteTemporaryFileByIdErrors, DeleteTemporaryFileByIdResponses, DeleteUserAvatarByIdData, DeleteUserAvatarByIdErrors, DeleteUserAvatarByIdResponses, DeleteUserById2FaByProviderNameData, DeleteUserById2FaByProviderNameErrors, DeleteUserById2FaByProviderNameResponses, DeleteUserByIdClientCredentialsByClientIdData, DeleteUserByIdClientCredentialsByClientIdErrors, DeleteUserByIdClientCredentialsByClientIdResponses, DeleteUserByIdData, DeleteUserByIdErrors, DeleteUserByIdResponses, DeleteUserCurrent2FaByProviderNameData, DeleteUserCurrent2FaByProviderNameErrors, DeleteUserCurrent2FaByProviderNameResponses, DeleteUserData, DeleteUserDataByIdData, DeleteUserDataByIdErrors, DeleteUserDataByIdResponses, DeleteUserErrors, DeleteUserGroupByIdData, DeleteUserGroupByIdErrors, DeleteUserGroupByIdResponses, DeleteUserGroupByIdUsersData, DeleteUserGroupByIdUsersErrors, DeleteUserGroupByIdUsersResponses, DeleteUserGroupData, DeleteUserGroupErrors, DeleteUserGroupResponses, DeleteUserResponses, DeleteWebhookByIdData, DeleteWebhookByIdErrors, DeleteWebhookByIdResponses, GetCollectionDocumentByIdData, GetCollectionDocumentByIdErrors, GetCollectionDocumentByIdResponses, GetCollectionMediaData, GetCollectionMediaErrors, GetCollectionMediaResponses, GetCultureData, GetCultureErrors, GetCultureResponses, GetDataTypeByIdData, GetDataTypeByIdErrors, GetDataTypeByIdIsUsedData, GetDataTypeByIdIsUsedErrors, GetDataTypeByIdIsUsedResponses, GetDataTypeByIdReferencedByData, GetDataTypeByIdReferencedByErrors, GetDataTypeByIdReferencedByResponses, GetDataTypeByIdResponses, GetDataTypeConfigurationData, GetDataTypeConfigurationErrors, GetDataTypeConfigurationResponses, GetDataTypeFolderByIdData, GetDataTypeFolderByIdErrors, GetDataTypeFolderByIdResponses, GetDictionaryByIdData, GetDictionaryByIdErrors, GetDictionaryByIdExportData, GetDictionaryByIdExportErrors, GetDictionaryByIdExportResponses, GetDictionaryByIdResponses, GetDictionaryData, GetDictionaryErrors, GetDictionaryResponses, GetDocumentAreReferencedData, GetDocumentAreReferencedErrors, GetDocumentAreReferencedResponses, GetDocumentBlueprintByIdData, GetDocumentBlueprintByIdErrors, GetDocumentBlueprintByIdResponses, GetDocumentBlueprintByIdScaffoldData, GetDocumentBlueprintByIdScaffoldErrors, GetDocumentBlueprintByIdScaffoldResponses, GetDocumentBlueprintFolderByIdData, GetDocumentBlueprintFolderByIdErrors, GetDocumentBlueprintFolderByIdResponses, GetDocumentByIdAuditLogData, GetDocumentByIdAuditLogErrors, GetDocumentByIdAuditLogResponses, GetDocumentByIdAvailableSegmentOptionsData, GetDocumentByIdAvailableSegmentOptionsErrors, GetDocumentByIdAvailableSegmentOptionsResponses, GetDocumentByIdData, GetDocumentByIdDomainsData, GetDocumentByIdDomainsErrors, GetDocumentByIdDomainsResponses, GetDocumentByIdErrors, GetDocumentByIdNotificationsData, GetDocumentByIdNotificationsErrors, GetDocumentByIdNotificationsResponses, GetDocumentByIdPreviewUrlData, GetDocumentByIdPreviewUrlErrors, GetDocumentByIdPreviewUrlResponses, GetDocumentByIdPublicAccessData, GetDocumentByIdPublicAccessErrors, GetDocumentByIdPublicAccessResponses, GetDocumentByIdPublishedData, GetDocumentByIdPublishedErrors, GetDocumentByIdPublishedResponses, GetDocumentByIdPublishWithDescendantsResultByTaskIdData, GetDocumentByIdPublishWithDescendantsResultByTaskIdErrors, GetDocumentByIdPublishWithDescendantsResultByTaskIdResponses, GetDocumentByIdReferencedByData, GetDocumentByIdReferencedByErrors, GetDocumentByIdReferencedByResponses, GetDocumentByIdReferencedDescendantsData, GetDocumentByIdReferencedDescendantsErrors, GetDocumentByIdReferencedDescendantsResponses, GetDocumentByIdResponses, GetDocumentConfigurationData, GetDocumentConfigurationErrors, GetDocumentConfigurationResponses, GetDocumentTypeAllowedAtRootData, GetDocumentTypeAllowedAtRootErrors, GetDocumentTypeAllowedAtRootResponses, GetDocumentTypeByIdAllowedChildrenData, GetDocumentTypeByIdAllowedChildrenErrors, GetDocumentTypeByIdAllowedChildrenResponses, GetDocumentTypeByIdBlueprintData, GetDocumentTypeByIdBlueprintErrors, GetDocumentTypeByIdBlueprintResponses, GetDocumentTypeByIdCompositionReferencesData, GetDocumentTypeByIdCompositionReferencesErrors, GetDocumentTypeByIdCompositionReferencesResponses, GetDocumentTypeByIdData, GetDocumentTypeByIdErrors, GetDocumentTypeByIdExportData, GetDocumentTypeByIdExportErrors, GetDocumentTypeByIdExportResponses, GetDocumentTypeByIdResponses, GetDocumentTypeConfigurationData, GetDocumentTypeConfigurationErrors, GetDocumentTypeConfigurationResponses, GetDocumentTypeFolderByIdData, GetDocumentTypeFolderByIdErrors, GetDocumentTypeFolderByIdResponses, GetDocumentUrlsData, GetDocumentUrlsErrors, GetDocumentUrlsResponses, GetDocumentVersionByIdData, GetDocumentVersionByIdErrors, GetDocumentVersionByIdResponses, GetDocumentVersionData, GetDocumentVersionErrors, GetDocumentVersionResponses, GetDynamicRootStepsData, GetDynamicRootStepsErrors, GetDynamicRootStepsResponses, GetFilterDataTypeData, GetFilterDataTypeErrors, GetFilterDataTypeResponses, GetFilterMemberData, GetFilterMemberErrors, GetFilterMemberResponses, GetFilterUserData, GetFilterUserErrors, GetFilterUserGroupData, GetFilterUserGroupErrors, GetFilterUserGroupResponses, GetFilterUserResponses, GetHealthCheckGroupByNameData, GetHealthCheckGroupByNameErrors, GetHealthCheckGroupByNameResponses, GetHealthCheckGroupData, GetHealthCheckGroupErrors, GetHealthCheckGroupResponses, GetHelpData, GetHelpErrors, GetHelpResponses, GetImagingResizeUrlsData, GetImagingResizeUrlsErrors, GetImagingResizeUrlsResponses, GetImportAnalyzeData, GetImportAnalyzeErrors, GetImportAnalyzeResponses, GetIndexerByIndexNameData, GetIndexerByIndexNameErrors, GetIndexerByIndexNameResponses, GetIndexerData, GetIndexerErrors, GetIndexerResponses, GetInstallSettingsData, GetInstallSettingsErrors, GetInstallSettingsResponses, GetItemDataTypeData, GetItemDataTypeErrors, GetItemDataTypeResponses, GetItemDataTypeSearchData, GetItemDataTypeSearchErrors, GetItemDataTypeSearchResponses, GetItemDictionaryData, GetItemDictionaryErrors, GetItemDictionaryResponses, GetItemDocumentBlueprintData, GetItemDocumentBlueprintErrors, GetItemDocumentBlueprintResponses, GetItemDocumentData, GetItemDocumentErrors, GetItemDocumentResponses, GetItemDocumentSearchData, GetItemDocumentSearchErrors, GetItemDocumentSearchResponses, GetItemDocumentTypeData, GetItemDocumentTypeErrors, GetItemDocumentTypeResponses, GetItemDocumentTypeSearchData, GetItemDocumentTypeSearchErrors, GetItemDocumentTypeSearchResponses, GetItemLanguageData, GetItemLanguageDefaultData, GetItemLanguageDefaultErrors, GetItemLanguageDefaultResponses, GetItemLanguageErrors, GetItemLanguageResponses, GetItemMediaData, GetItemMediaErrors, GetItemMediaResponses, GetItemMediaSearchData, GetItemMediaSearchErrors, GetItemMediaSearchResponses, GetItemMediaTypeAllowedData, GetItemMediaTypeAllowedErrors, GetItemMediaTypeAllowedResponses, GetItemMediaTypeData, GetItemMediaTypeErrors, GetItemMediaTypeFoldersData, GetItemMediaTypeFoldersErrors, GetItemMediaTypeFoldersResponses, GetItemMediaTypeResponses, GetItemMediaTypeSearchData, GetItemMediaTypeSearchErrors, GetItemMediaTypeSearchResponses, GetItemMemberData, GetItemMemberErrors, GetItemMemberGroupData, GetItemMemberGroupErrors, GetItemMemberGroupResponses, GetItemMemberResponses, GetItemMemberSearchData, GetItemMemberSearchErrors, GetItemMemberSearchResponses, GetItemMemberTypeData, GetItemMemberTypeErrors, GetItemMemberTypeResponses, GetItemMemberTypeSearchData, GetItemMemberTypeSearchErrors, GetItemMemberTypeSearchResponses, GetItemPartialViewData, GetItemPartialViewErrors, GetItemPartialViewResponses, GetItemRelationTypeData, GetItemRelationTypeErrors, GetItemRelationTypeResponses, GetItemScriptData, GetItemScriptErrors, GetItemScriptResponses, GetItemStaticFileData, GetItemStaticFileErrors, GetItemStaticFileResponses, GetItemStylesheetData, GetItemStylesheetErrors, GetItemStylesheetResponses, GetItemTemplateData, GetItemTemplateErrors, GetItemTemplateResponses, GetItemTemplateSearchData, GetItemTemplateSearchErrors, GetItemTemplateSearchResponses, GetItemUserData, GetItemUserErrors, GetItemUserGroupData, GetItemUserGroupErrors, GetItemUserGroupResponses, GetItemUserResponses, GetItemWebhookData, GetItemWebhookErrors, GetItemWebhookResponses, GetLanguageByIsoCodeData, GetLanguageByIsoCodeErrors, GetLanguageByIsoCodeResponses, GetLanguageData, GetLanguageErrors, GetLanguageResponses, GetLogViewerLevelCountData, GetLogViewerLevelCountErrors, GetLogViewerLevelCountResponses, GetLogViewerLevelData, GetLogViewerLevelErrors, GetLogViewerLevelResponses, GetLogViewerLogData, GetLogViewerLogErrors, GetLogViewerLogResponses, GetLogViewerMessageTemplateData, GetLogViewerMessageTemplateErrors, GetLogViewerMessageTemplateResponses, GetLogViewerSavedSearchByNameData, GetLogViewerSavedSearchByNameErrors, GetLogViewerSavedSearchByNameResponses, GetLogViewerSavedSearchData, GetLogViewerSavedSearchErrors, GetLogViewerSavedSearchResponses, GetLogViewerValidateLogsSizeData, GetLogViewerValidateLogsSizeErrors, GetLogViewerValidateLogsSizeResponses, GetManifestManifestData, GetManifestManifestErrors, GetManifestManifestPrivateData, GetManifestManifestPrivateErrors, GetManifestManifestPrivateResponses, GetManifestManifestPublicData, GetManifestManifestPublicResponses, GetManifestManifestResponses, GetMediaAreReferencedData, GetMediaAreReferencedErrors, GetMediaAreReferencedResponses, GetMediaByIdAuditLogData, GetMediaByIdAuditLogErrors, GetMediaByIdAuditLogResponses, GetMediaByIdData, GetMediaByIdErrors, GetMediaByIdReferencedByData, GetMediaByIdReferencedByErrors, GetMediaByIdReferencedByResponses, GetMediaByIdReferencedDescendantsData, GetMediaByIdReferencedDescendantsErrors, GetMediaByIdReferencedDescendantsResponses, GetMediaByIdResponses, GetMediaConfigurationData, GetMediaConfigurationErrors, GetMediaConfigurationResponses, GetMediaTypeAllowedAtRootData, GetMediaTypeAllowedAtRootErrors, GetMediaTypeAllowedAtRootResponses, GetMediaTypeByIdAllowedChildrenData, GetMediaTypeByIdAllowedChildrenErrors, GetMediaTypeByIdAllowedChildrenResponses, GetMediaTypeByIdCompositionReferencesData, GetMediaTypeByIdCompositionReferencesErrors, GetMediaTypeByIdCompositionReferencesResponses, GetMediaTypeByIdData, GetMediaTypeByIdErrors, GetMediaTypeByIdExportData, GetMediaTypeByIdExportErrors, GetMediaTypeByIdExportResponses, GetMediaTypeByIdResponses, GetMediaTypeConfigurationData, GetMediaTypeConfigurationErrors, GetMediaTypeConfigurationResponses, GetMediaTypeFolderByIdData, GetMediaTypeFolderByIdErrors, GetMediaTypeFolderByIdResponses, GetMediaUrlsData, GetMediaUrlsErrors, GetMediaUrlsResponses, GetMemberAreReferencedData, GetMemberAreReferencedErrors, GetMemberAreReferencedResponses, GetMemberByIdData, GetMemberByIdErrors, GetMemberByIdReferencedByData, GetMemberByIdReferencedByErrors, GetMemberByIdReferencedByResponses, GetMemberByIdReferencedDescendantsData, GetMemberByIdReferencedDescendantsErrors, GetMemberByIdReferencedDescendantsResponses, GetMemberByIdResponses, GetMemberConfigurationData, GetMemberConfigurationErrors, GetMemberConfigurationResponses, GetMemberGroupByIdData, GetMemberGroupByIdErrors, GetMemberGroupByIdResponses, GetMemberGroupData, GetMemberGroupErrors, GetMemberGroupResponses, GetMemberTypeByIdCompositionReferencesData, GetMemberTypeByIdCompositionReferencesErrors, GetMemberTypeByIdCompositionReferencesResponses, GetMemberTypeByIdData, GetMemberTypeByIdErrors, GetMemberTypeByIdResponses, GetMemberTypeConfigurationData, GetMemberTypeConfigurationErrors, GetMemberTypeConfigurationResponses, GetModelsBuilderDashboardData, GetModelsBuilderDashboardErrors, GetModelsBuilderDashboardResponses, GetModelsBuilderStatusData, GetModelsBuilderStatusErrors, GetModelsBuilderStatusResponses, GetObjectTypesData, GetObjectTypesErrors, GetObjectTypesResponses, GetOembedQueryData, GetOembedQueryErrors, GetOembedQueryResponses, GetPackageConfigurationData, GetPackageConfigurationErrors, GetPackageConfigurationResponses, GetPackageCreatedByIdData, GetPackageCreatedByIdDownloadData, GetPackageCreatedByIdDownloadErrors, GetPackageCreatedByIdDownloadResponses, GetPackageCreatedByIdErrors, GetPackageCreatedByIdResponses, GetPackageCreatedData, GetPackageCreatedErrors, GetPackageCreatedResponses, GetPackageMigrationStatusData, GetPackageMigrationStatusErrors, GetPackageMigrationStatusResponses, GetPartialViewByPathData, GetPartialViewByPathErrors, GetPartialViewByPathResponses, GetPartialViewFolderByPathData, GetPartialViewFolderByPathErrors, GetPartialViewFolderByPathResponses, GetPartialViewSnippetByIdData, GetPartialViewSnippetByIdErrors, GetPartialViewSnippetByIdResponses, GetPartialViewSnippetData, GetPartialViewSnippetErrors, GetPartialViewSnippetResponses, GetProfilingStatusData, GetProfilingStatusErrors, GetProfilingStatusResponses, GetPropertyTypeIsUsedData, GetPropertyTypeIsUsedErrors, GetPropertyTypeIsUsedResponses, GetPublishedCacheRebuildStatusData, GetPublishedCacheRebuildStatusErrors, GetPublishedCacheRebuildStatusResponses, GetRecycleBinDocumentByIdOriginalParentData, GetRecycleBinDocumentByIdOriginalParentErrors, GetRecycleBinDocumentByIdOriginalParentResponses, GetRecycleBinDocumentChildrenData, GetRecycleBinDocumentChildrenErrors, GetRecycleBinDocumentChildrenResponses, GetRecycleBinDocumentReferencedByData, GetRecycleBinDocumentReferencedByErrors, GetRecycleBinDocumentReferencedByResponses, GetRecycleBinDocumentRootData, GetRecycleBinDocumentRootErrors, GetRecycleBinDocumentRootResponses, GetRecycleBinDocumentSiblingsData, GetRecycleBinDocumentSiblingsErrors, GetRecycleBinDocumentSiblingsResponses, GetRecycleBinMediaByIdOriginalParentData, GetRecycleBinMediaByIdOriginalParentErrors, GetRecycleBinMediaByIdOriginalParentResponses, GetRecycleBinMediaChildrenData, GetRecycleBinMediaChildrenErrors, GetRecycleBinMediaChildrenResponses, GetRecycleBinMediaReferencedByData, GetRecycleBinMediaReferencedByErrors, GetRecycleBinMediaReferencedByResponses, GetRecycleBinMediaRootData, GetRecycleBinMediaRootErrors, GetRecycleBinMediaRootResponses, GetRecycleBinMediaSiblingsData, GetRecycleBinMediaSiblingsErrors, GetRecycleBinMediaSiblingsResponses, GetRedirectManagementByIdData, GetRedirectManagementByIdErrors, GetRedirectManagementByIdResponses, GetRedirectManagementData, GetRedirectManagementErrors, GetRedirectManagementResponses, GetRedirectManagementStatusData, GetRedirectManagementStatusErrors, GetRedirectManagementStatusResponses, GetRelationByRelationTypeIdData, GetRelationByRelationTypeIdErrors, GetRelationByRelationTypeIdResponses, GetRelationTypeByIdData, GetRelationTypeByIdErrors, GetRelationTypeByIdResponses, GetRelationTypeData, GetRelationTypeErrors, GetRelationTypeResponses, GetScriptByPathData, GetScriptByPathErrors, GetScriptByPathResponses, GetScriptFolderByPathData, GetScriptFolderByPathErrors, GetScriptFolderByPathResponses, GetSearcherBySearcherNameQueryData, GetSearcherBySearcherNameQueryErrors, GetSearcherBySearcherNameQueryResponses, GetSearcherData, GetSearcherErrors, GetSearcherResponses, GetSecurityConfigurationData, GetSecurityConfigurationErrors, GetSecurityConfigurationResponses, GetSegmentData, GetSegmentErrors, GetSegmentResponses, GetServerConfigurationData, GetServerConfigurationResponses, GetServerInformationData, GetServerInformationErrors, GetServerInformationResponses, GetServerStatusData, GetServerStatusErrors, GetServerStatusResponses, GetServerTroubleshootingData, GetServerTroubleshootingErrors, GetServerTroubleshootingResponses, GetServerUpgradeCheckData, GetServerUpgradeCheckErrors, GetServerUpgradeCheckResponses, GetStylesheetByPathData, GetStylesheetByPathErrors, GetStylesheetByPathResponses, GetStylesheetFolderByPathData, GetStylesheetFolderByPathErrors, GetStylesheetFolderByPathResponses, GetTagData, GetTagErrors, GetTagResponses, GetTelemetryData, GetTelemetryErrors, GetTelemetryLevelData, GetTelemetryLevelErrors, GetTelemetryLevelResponses, GetTelemetryResponses, GetTemplateByIdData, GetTemplateByIdErrors, GetTemplateByIdResponses, GetTemplateConfigurationData, GetTemplateConfigurationErrors, GetTemplateConfigurationResponses, GetTemplateQuerySettingsData, GetTemplateQuerySettingsErrors, GetTemplateQuerySettingsResponses, GetTemporaryFileByIdData, GetTemporaryFileByIdErrors, GetTemporaryFileByIdResponses, GetTemporaryFileConfigurationData, GetTemporaryFileConfigurationErrors, GetTemporaryFileConfigurationResponses, GetTreeDataTypeAncestorsData, GetTreeDataTypeAncestorsErrors, GetTreeDataTypeAncestorsResponses, GetTreeDataTypeChildrenData, GetTreeDataTypeChildrenErrors, GetTreeDataTypeChildrenResponses, GetTreeDataTypeRootData, GetTreeDataTypeRootErrors, GetTreeDataTypeRootResponses, GetTreeDataTypeSiblingsData, GetTreeDataTypeSiblingsErrors, GetTreeDataTypeSiblingsResponses, GetTreeDictionaryAncestorsData, GetTreeDictionaryAncestorsErrors, GetTreeDictionaryAncestorsResponses, GetTreeDictionaryChildrenData, GetTreeDictionaryChildrenErrors, GetTreeDictionaryChildrenResponses, GetTreeDictionaryRootData, GetTreeDictionaryRootErrors, GetTreeDictionaryRootResponses, GetTreeDocumentAncestorsData, GetTreeDocumentAncestorsErrors, GetTreeDocumentAncestorsResponses, GetTreeDocumentBlueprintAncestorsData, GetTreeDocumentBlueprintAncestorsErrors, GetTreeDocumentBlueprintAncestorsResponses, GetTreeDocumentBlueprintChildrenData, GetTreeDocumentBlueprintChildrenErrors, GetTreeDocumentBlueprintChildrenResponses, GetTreeDocumentBlueprintRootData, GetTreeDocumentBlueprintRootErrors, GetTreeDocumentBlueprintRootResponses, GetTreeDocumentBlueprintSiblingsData, GetTreeDocumentBlueprintSiblingsErrors, GetTreeDocumentBlueprintSiblingsResponses, GetTreeDocumentChildrenData, GetTreeDocumentChildrenErrors, GetTreeDocumentChildrenResponses, GetTreeDocumentRootData, GetTreeDocumentRootErrors, GetTreeDocumentRootResponses, GetTreeDocumentSiblingsData, GetTreeDocumentSiblingsErrors, GetTreeDocumentSiblingsResponses, GetTreeDocumentTypeAncestorsData, GetTreeDocumentTypeAncestorsErrors, GetTreeDocumentTypeAncestorsResponses, GetTreeDocumentTypeChildrenData, GetTreeDocumentTypeChildrenErrors, GetTreeDocumentTypeChildrenResponses, GetTreeDocumentTypeRootData, GetTreeDocumentTypeRootErrors, GetTreeDocumentTypeRootResponses, GetTreeDocumentTypeSiblingsData, GetTreeDocumentTypeSiblingsErrors, GetTreeDocumentTypeSiblingsResponses, GetTreeMediaAncestorsData, GetTreeMediaAncestorsErrors, GetTreeMediaAncestorsResponses, GetTreeMediaChildrenData, GetTreeMediaChildrenErrors, GetTreeMediaChildrenResponses, GetTreeMediaRootData, GetTreeMediaRootErrors, GetTreeMediaRootResponses, GetTreeMediaSiblingsData, GetTreeMediaSiblingsErrors, GetTreeMediaSiblingsResponses, GetTreeMediaTypeAncestorsData, GetTreeMediaTypeAncestorsErrors, GetTreeMediaTypeAncestorsResponses, GetTreeMediaTypeChildrenData, GetTreeMediaTypeChildrenErrors, GetTreeMediaTypeChildrenResponses, GetTreeMediaTypeRootData, GetTreeMediaTypeRootErrors, GetTreeMediaTypeRootResponses, GetTreeMediaTypeSiblingsData, GetTreeMediaTypeSiblingsErrors, GetTreeMediaTypeSiblingsResponses, GetTreeMemberGroupRootData, GetTreeMemberGroupRootErrors, GetTreeMemberGroupRootResponses, GetTreeMemberTypeRootData, GetTreeMemberTypeRootErrors, GetTreeMemberTypeRootResponses, GetTreeMemberTypeSiblingsData, GetTreeMemberTypeSiblingsErrors, GetTreeMemberTypeSiblingsResponses, GetTreePartialViewAncestorsData, GetTreePartialViewAncestorsErrors, GetTreePartialViewAncestorsResponses, GetTreePartialViewChildrenData, GetTreePartialViewChildrenErrors, GetTreePartialViewChildrenResponses, GetTreePartialViewRootData, GetTreePartialViewRootErrors, GetTreePartialViewRootResponses, GetTreePartialViewSiblingsData, GetTreePartialViewSiblingsErrors, GetTreePartialViewSiblingsResponses, GetTreeScriptAncestorsData, GetTreeScriptAncestorsErrors, GetTreeScriptAncestorsResponses, GetTreeScriptChildrenData, GetTreeScriptChildrenErrors, GetTreeScriptChildrenResponses, GetTreeScriptRootData, GetTreeScriptRootErrors, GetTreeScriptRootResponses, GetTreeScriptSiblingsData, GetTreeScriptSiblingsErrors, GetTreeScriptSiblingsResponses, GetTreeStaticFileAncestorsData, GetTreeStaticFileAncestorsErrors, GetTreeStaticFileAncestorsResponses, GetTreeStaticFileChildrenData, GetTreeStaticFileChildrenErrors, GetTreeStaticFileChildrenResponses, GetTreeStaticFileRootData, GetTreeStaticFileRootErrors, GetTreeStaticFileRootResponses, GetTreeStylesheetAncestorsData, GetTreeStylesheetAncestorsErrors, GetTreeStylesheetAncestorsResponses, GetTreeStylesheetChildrenData, GetTreeStylesheetChildrenErrors, GetTreeStylesheetChildrenResponses, GetTreeStylesheetRootData, GetTreeStylesheetRootErrors, GetTreeStylesheetRootResponses, GetTreeStylesheetSiblingsData, GetTreeStylesheetSiblingsErrors, GetTreeStylesheetSiblingsResponses, GetTreeTemplateAncestorsData, GetTreeTemplateAncestorsErrors, GetTreeTemplateAncestorsResponses, GetTreeTemplateChildrenData, GetTreeTemplateChildrenErrors, GetTreeTemplateChildrenResponses, GetTreeTemplateRootData, GetTreeTemplateRootErrors, GetTreeTemplateRootResponses, GetTreeTemplateSiblingsData, GetTreeTemplateSiblingsErrors, GetTreeTemplateSiblingsResponses, GetUpgradeSettingsData, GetUpgradeSettingsErrors, GetUpgradeSettingsResponses, GetUserById2FaData, GetUserById2FaErrors, GetUserById2FaResponses, GetUserByIdCalculateStartNodesData, GetUserByIdCalculateStartNodesErrors, GetUserByIdCalculateStartNodesResponses, GetUserByIdClientCredentialsData, GetUserByIdClientCredentialsErrors, GetUserByIdClientCredentialsResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, GetUserConfigurationData, GetUserConfigurationErrors, GetUserConfigurationResponses, GetUserCurrent2FaByProviderNameData, GetUserCurrent2FaByProviderNameErrors, GetUserCurrent2FaByProviderNameResponses, GetUserCurrent2FaData, GetUserCurrent2FaErrors, GetUserCurrent2FaResponses, GetUserCurrentConfigurationData, GetUserCurrentConfigurationErrors, GetUserCurrentConfigurationResponses, GetUserCurrentData, GetUserCurrentErrors, GetUserCurrentLoginProvidersData, GetUserCurrentLoginProvidersErrors, GetUserCurrentLoginProvidersResponses, GetUserCurrentPermissionsData, GetUserCurrentPermissionsDocumentData, GetUserCurrentPermissionsDocumentErrors, GetUserCurrentPermissionsDocumentResponses, GetUserCurrentPermissionsErrors, GetUserCurrentPermissionsMediaData, GetUserCurrentPermissionsMediaErrors, GetUserCurrentPermissionsMediaResponses, GetUserCurrentPermissionsResponses, GetUserCurrentResponses, GetUserData, GetUserDataByIdData, GetUserDataByIdErrors, GetUserDataByIdResponses, GetUserDataData, GetUserDataErrors, GetUserDataResponses, GetUserErrors, GetUserGroupByIdData, GetUserGroupByIdErrors, GetUserGroupByIdResponses, GetUserGroupData, GetUserGroupErrors, GetUserGroupResponses, GetUserResponses, GetWebhookByIdData, GetWebhookByIdErrors, GetWebhookByIdLogsData, GetWebhookByIdLogsErrors, GetWebhookByIdLogsResponses, GetWebhookByIdResponses, GetWebhookData, GetWebhookErrors, GetWebhookEventsData, GetWebhookEventsErrors, GetWebhookEventsResponses, GetWebhookLogsData, GetWebhookLogsErrors, GetWebhookLogsResponses, GetWebhookResponses, PostDataTypeByIdCopyData, PostDataTypeByIdCopyErrors, PostDataTypeByIdCopyResponses, PostDataTypeData, PostDataTypeErrors, PostDataTypeFolderData, PostDataTypeFolderErrors, PostDataTypeFolderResponses, PostDataTypeResponses, PostDictionaryData, PostDictionaryErrors, PostDictionaryImportData, PostDictionaryImportErrors, PostDictionaryImportResponses, PostDictionaryResponses, PostDocumentBlueprintData, PostDocumentBlueprintErrors, PostDocumentBlueprintFolderData, PostDocumentBlueprintFolderErrors, PostDocumentBlueprintFolderResponses, PostDocumentBlueprintFromDocumentData, PostDocumentBlueprintFromDocumentErrors, PostDocumentBlueprintFromDocumentResponses, PostDocumentBlueprintResponses, PostDocumentByIdCopyData, PostDocumentByIdCopyErrors, PostDocumentByIdCopyResponses, PostDocumentByIdPublicAccessData, PostDocumentByIdPublicAccessErrors, PostDocumentByIdPublicAccessResponses, PostDocumentData, PostDocumentErrors, PostDocumentResponses, PostDocumentTypeAvailableCompositionsData, PostDocumentTypeAvailableCompositionsErrors, PostDocumentTypeAvailableCompositionsResponses, PostDocumentTypeByIdCopyData, PostDocumentTypeByIdCopyErrors, PostDocumentTypeByIdCopyResponses, PostDocumentTypeData, PostDocumentTypeErrors, PostDocumentTypeFolderData, PostDocumentTypeFolderErrors, PostDocumentTypeFolderResponses, PostDocumentTypeImportData, PostDocumentTypeImportErrors, PostDocumentTypeImportResponses, PostDocumentTypeResponses, PostDocumentValidateData, PostDocumentValidateErrors, PostDocumentValidateResponses, PostDocumentVersionByIdRollbackData, PostDocumentVersionByIdRollbackErrors, PostDocumentVersionByIdRollbackResponses, PostDynamicRootQueryData, PostDynamicRootQueryErrors, PostDynamicRootQueryResponses, PostHealthCheckExecuteActionData, PostHealthCheckExecuteActionErrors, PostHealthCheckExecuteActionResponses, PostHealthCheckGroupByNameCheckData, PostHealthCheckGroupByNameCheckErrors, PostHealthCheckGroupByNameCheckResponses, PostIndexerByIndexNameRebuildData, PostIndexerByIndexNameRebuildErrors, PostIndexerByIndexNameRebuildResponses, PostInstallSetupData, PostInstallSetupErrors, PostInstallSetupResponses, PostInstallValidateDatabaseData, PostInstallValidateDatabaseErrors, PostInstallValidateDatabaseResponses, PostLanguageData, PostLanguageErrors, PostLanguageResponses, PostLogViewerSavedSearchData, PostLogViewerSavedSearchErrors, PostLogViewerSavedSearchResponses, PostMediaData, PostMediaErrors, PostMediaResponses, PostMediaTypeAvailableCompositionsData, PostMediaTypeAvailableCompositionsErrors, PostMediaTypeAvailableCompositionsResponses, PostMediaTypeByIdCopyData, PostMediaTypeByIdCopyErrors, PostMediaTypeByIdCopyResponses, PostMediaTypeData, PostMediaTypeErrors, PostMediaTypeFolderData, PostMediaTypeFolderErrors, PostMediaTypeFolderResponses, PostMediaTypeImportData, PostMediaTypeImportErrors, PostMediaTypeImportResponses, PostMediaTypeResponses, PostMediaValidateData, PostMediaValidateErrors, PostMediaValidateResponses, PostMemberData, PostMemberErrors, PostMemberGroupData, PostMemberGroupErrors, PostMemberGroupResponses, PostMemberResponses, PostMemberTypeAvailableCompositionsData, PostMemberTypeAvailableCompositionsErrors, PostMemberTypeAvailableCompositionsResponses, PostMemberTypeByIdCopyData, PostMemberTypeByIdCopyErrors, PostMemberTypeByIdCopyResponses, PostMemberTypeData, PostMemberTypeErrors, PostMemberTypeResponses, PostMemberValidateData, PostMemberValidateErrors, PostMemberValidateResponses, PostModelsBuilderBuildData, PostModelsBuilderBuildErrors, PostModelsBuilderBuildResponses, PostPackageByNameRunMigrationData, PostPackageByNameRunMigrationErrors, PostPackageByNameRunMigrationResponses, PostPackageCreatedData, PostPackageCreatedErrors, PostPackageCreatedResponses, PostPartialViewData, PostPartialViewErrors, PostPartialViewFolderData, PostPartialViewFolderErrors, PostPartialViewFolderResponses, PostPartialViewResponses, PostPreviewData, PostPreviewErrors, PostPreviewResponses, PostPublishedCacheRebuildData, PostPublishedCacheRebuildErrors, PostPublishedCacheRebuildResponses, PostPublishedCacheReloadData, PostPublishedCacheReloadErrors, PostPublishedCacheReloadResponses, PostRedirectManagementStatusData, PostRedirectManagementStatusErrors, PostRedirectManagementStatusResponses, PostScriptData, PostScriptErrors, PostScriptFolderData, PostScriptFolderErrors, PostScriptFolderResponses, PostScriptResponses, PostSecurityForgotPasswordData, PostSecurityForgotPasswordErrors, PostSecurityForgotPasswordResetData, PostSecurityForgotPasswordResetErrors, PostSecurityForgotPasswordResetResponses, PostSecurityForgotPasswordResponses, PostSecurityForgotPasswordVerifyData, PostSecurityForgotPasswordVerifyErrors, PostSecurityForgotPasswordVerifyResponses, PostStylesheetData, PostStylesheetErrors, PostStylesheetFolderData, PostStylesheetFolderErrors, PostStylesheetFolderResponses, PostStylesheetResponses, PostTelemetryLevelData, PostTelemetryLevelErrors, PostTelemetryLevelResponses, PostTemplateData, PostTemplateErrors, PostTemplateQueryExecuteData, PostTemplateQueryExecuteErrors, PostTemplateQueryExecuteResponses, PostTemplateResponses, PostTemporaryFileData, PostTemporaryFileErrors, PostTemporaryFileResponses, PostUpgradeAuthorizeData, PostUpgradeAuthorizeErrors, PostUpgradeAuthorizeResponses, PostUserAvatarByIdData, PostUserAvatarByIdErrors, PostUserAvatarByIdResponses, PostUserByIdChangePasswordData, PostUserByIdChangePasswordErrors, PostUserByIdChangePasswordResponses, PostUserByIdClientCredentialsData, PostUserByIdClientCredentialsErrors, PostUserByIdClientCredentialsResponses, PostUserByIdResetPasswordData, PostUserByIdResetPasswordErrors, PostUserByIdResetPasswordResponses, PostUserCurrent2FaByProviderNameData, PostUserCurrent2FaByProviderNameErrors, PostUserCurrent2FaByProviderNameResponses, PostUserCurrentAvatarData, PostUserCurrentAvatarErrors, PostUserCurrentAvatarResponses, PostUserCurrentChangePasswordData, PostUserCurrentChangePasswordErrors, PostUserCurrentChangePasswordResponses, PostUserData, PostUserDataData, PostUserDataErrors, PostUserDataResponses, PostUserDisableData, PostUserDisableErrors, PostUserDisableResponses, PostUserEnableData, PostUserEnableErrors, PostUserEnableResponses, PostUserErrors, PostUserGroupByIdUsersData, PostUserGroupByIdUsersErrors, PostUserGroupByIdUsersResponses, PostUserGroupData, PostUserGroupErrors, PostUserGroupResponses, PostUserInviteCreatePasswordData, PostUserInviteCreatePasswordErrors, PostUserInviteCreatePasswordResponses, PostUserInviteData, PostUserInviteErrors, PostUserInviteResendData, PostUserInviteResendErrors, PostUserInviteResendResponses, PostUserInviteResponses, PostUserInviteVerifyData, PostUserInviteVerifyErrors, PostUserInviteVerifyResponses, PostUserResponses, PostUserSetUserGroupsData, PostUserSetUserGroupsErrors, PostUserSetUserGroupsResponses, PostUserUnlockData, PostUserUnlockErrors, PostUserUnlockResponses, PostWebhookData, PostWebhookErrors, PostWebhookResponses, PutDataTypeByIdData, PutDataTypeByIdErrors, PutDataTypeByIdMoveData, PutDataTypeByIdMoveErrors, PutDataTypeByIdMoveResponses, PutDataTypeByIdResponses, PutDataTypeFolderByIdData, PutDataTypeFolderByIdErrors, PutDataTypeFolderByIdResponses, PutDictionaryByIdData, PutDictionaryByIdErrors, PutDictionaryByIdMoveData, PutDictionaryByIdMoveErrors, PutDictionaryByIdMoveResponses, PutDictionaryByIdResponses, PutDocumentBlueprintByIdData, PutDocumentBlueprintByIdErrors, PutDocumentBlueprintByIdMoveData, PutDocumentBlueprintByIdMoveErrors, PutDocumentBlueprintByIdMoveResponses, PutDocumentBlueprintByIdResponses, PutDocumentBlueprintFolderByIdData, PutDocumentBlueprintFolderByIdErrors, PutDocumentBlueprintFolderByIdResponses, PutDocumentByIdData, PutDocumentByIdDomainsData, PutDocumentByIdDomainsErrors, PutDocumentByIdDomainsResponses, PutDocumentByIdErrors, PutDocumentByIdMoveData, PutDocumentByIdMoveErrors, PutDocumentByIdMoveResponses, PutDocumentByIdMoveToRecycleBinData, PutDocumentByIdMoveToRecycleBinErrors, PutDocumentByIdMoveToRecycleBinResponses, PutDocumentByIdNotificationsData, PutDocumentByIdNotificationsErrors, PutDocumentByIdNotificationsResponses, PutDocumentByIdPublicAccessData, PutDocumentByIdPublicAccessErrors, PutDocumentByIdPublicAccessResponses, PutDocumentByIdPublishData, PutDocumentByIdPublishErrors, PutDocumentByIdPublishResponses, PutDocumentByIdPublishWithDescendantsData, PutDocumentByIdPublishWithDescendantsErrors, PutDocumentByIdPublishWithDescendantsResponses, PutDocumentByIdResponses, PutDocumentByIdUnpublishData, PutDocumentByIdUnpublishErrors, PutDocumentByIdUnpublishResponses, PutDocumentSortData, PutDocumentSortErrors, PutDocumentSortResponses, PutDocumentTypeByIdData, PutDocumentTypeByIdErrors, PutDocumentTypeByIdImportData, PutDocumentTypeByIdImportErrors, PutDocumentTypeByIdImportResponses, PutDocumentTypeByIdMoveData, PutDocumentTypeByIdMoveErrors, PutDocumentTypeByIdMoveResponses, PutDocumentTypeByIdResponses, PutDocumentTypeFolderByIdData, PutDocumentTypeFolderByIdErrors, PutDocumentTypeFolderByIdResponses, PutDocumentVersionByIdPreventCleanupData, PutDocumentVersionByIdPreventCleanupErrors, PutDocumentVersionByIdPreventCleanupResponses, PutLanguageByIsoCodeData, PutLanguageByIsoCodeErrors, PutLanguageByIsoCodeResponses, PutMediaByIdData, PutMediaByIdErrors, PutMediaByIdMoveData, PutMediaByIdMoveErrors, PutMediaByIdMoveResponses, PutMediaByIdMoveToRecycleBinData, PutMediaByIdMoveToRecycleBinErrors, PutMediaByIdMoveToRecycleBinResponses, PutMediaByIdResponses, PutMediaByIdValidateData, PutMediaByIdValidateErrors, PutMediaByIdValidateResponses, PutMediaSortData, PutMediaSortErrors, PutMediaSortResponses, PutMediaTypeByIdData, PutMediaTypeByIdErrors, PutMediaTypeByIdImportData, PutMediaTypeByIdImportErrors, PutMediaTypeByIdImportResponses, PutMediaTypeByIdMoveData, PutMediaTypeByIdMoveErrors, PutMediaTypeByIdMoveResponses, PutMediaTypeByIdResponses, PutMediaTypeFolderByIdData, PutMediaTypeFolderByIdErrors, PutMediaTypeFolderByIdResponses, PutMemberByIdData, PutMemberByIdErrors, PutMemberByIdResponses, PutMemberByIdValidateData, PutMemberByIdValidateErrors, PutMemberByIdValidateResponses, PutMemberGroupByIdData, PutMemberGroupByIdErrors, PutMemberGroupByIdResponses, PutMemberTypeByIdData, PutMemberTypeByIdErrors, PutMemberTypeByIdResponses, PutPackageCreatedByIdData, PutPackageCreatedByIdErrors, PutPackageCreatedByIdResponses, PutPartialViewByPathData, PutPartialViewByPathErrors, PutPartialViewByPathRenameData, PutPartialViewByPathRenameErrors, PutPartialViewByPathRenameResponses, PutPartialViewByPathResponses, PutProfilingStatusData, PutProfilingStatusErrors, PutProfilingStatusResponses, PutRecycleBinDocumentByIdRestoreData, PutRecycleBinDocumentByIdRestoreErrors, PutRecycleBinDocumentByIdRestoreResponses, PutRecycleBinMediaByIdRestoreData, PutRecycleBinMediaByIdRestoreErrors, PutRecycleBinMediaByIdRestoreResponses, PutScriptByPathData, PutScriptByPathErrors, PutScriptByPathRenameData, PutScriptByPathRenameErrors, PutScriptByPathRenameResponses, PutScriptByPathResponses, PutStylesheetByPathData, PutStylesheetByPathErrors, PutStylesheetByPathRenameData, PutStylesheetByPathRenameErrors, PutStylesheetByPathRenameResponses, PutStylesheetByPathResponses, PutTemplateByIdData, PutTemplateByIdErrors, PutTemplateByIdResponses, PutUmbracoManagementApiV11DocumentByIdValidate11Data, PutUmbracoManagementApiV11DocumentByIdValidate11Errors, PutUmbracoManagementApiV11DocumentByIdValidate11Responses, PutUserByIdData, PutUserByIdErrors, PutUserByIdResponses, PutUserDataData, PutUserDataErrors, PutUserDataResponses, PutUserGroupByIdData, PutUserGroupByIdErrors, PutUserGroupByIdResponses, PutWebhookByIdData, PutWebhookByIdErrors, PutWebhookByIdResponses } from './types.gen'; +import type { DeleteDataTypeByIdData, DeleteDataTypeByIdErrors, DeleteDataTypeByIdResponses, DeleteDataTypeFolderByIdData, DeleteDataTypeFolderByIdErrors, DeleteDataTypeFolderByIdResponses, DeleteDictionaryByIdData, DeleteDictionaryByIdErrors, DeleteDictionaryByIdResponses, DeleteDocumentBlueprintByIdData, DeleteDocumentBlueprintByIdErrors, DeleteDocumentBlueprintByIdResponses, DeleteDocumentBlueprintFolderByIdData, DeleteDocumentBlueprintFolderByIdErrors, DeleteDocumentBlueprintFolderByIdResponses, DeleteDocumentByIdData, DeleteDocumentByIdErrors, DeleteDocumentByIdPublicAccessData, DeleteDocumentByIdPublicAccessErrors, DeleteDocumentByIdPublicAccessResponses, DeleteDocumentByIdResponses, DeleteDocumentTypeByIdData, DeleteDocumentTypeByIdErrors, DeleteDocumentTypeByIdResponses, DeleteDocumentTypeFolderByIdData, DeleteDocumentTypeFolderByIdErrors, DeleteDocumentTypeFolderByIdResponses, DeleteLanguageByIsoCodeData, DeleteLanguageByIsoCodeErrors, DeleteLanguageByIsoCodeResponses, DeleteLogViewerSavedSearchByNameData, DeleteLogViewerSavedSearchByNameErrors, DeleteLogViewerSavedSearchByNameResponses, DeleteMediaByIdData, DeleteMediaByIdErrors, DeleteMediaByIdResponses, DeleteMediaTypeByIdData, DeleteMediaTypeByIdErrors, DeleteMediaTypeByIdResponses, DeleteMediaTypeFolderByIdData, DeleteMediaTypeFolderByIdErrors, DeleteMediaTypeFolderByIdResponses, DeleteMemberByIdData, DeleteMemberByIdErrors, DeleteMemberByIdResponses, DeleteMemberGroupByIdData, DeleteMemberGroupByIdErrors, DeleteMemberGroupByIdResponses, DeleteMemberTypeByIdData, DeleteMemberTypeByIdErrors, DeleteMemberTypeByIdResponses, DeleteMemberTypeFolderByIdData, DeleteMemberTypeFolderByIdErrors, DeleteMemberTypeFolderByIdResponses, DeletePackageCreatedByIdData, DeletePackageCreatedByIdErrors, DeletePackageCreatedByIdResponses, DeletePartialViewByPathData, DeletePartialViewByPathErrors, DeletePartialViewByPathResponses, DeletePartialViewFolderByPathData, DeletePartialViewFolderByPathErrors, DeletePartialViewFolderByPathResponses, DeletePreviewData, DeletePreviewResponses, DeleteRecycleBinDocumentByIdData, DeleteRecycleBinDocumentByIdErrors, DeleteRecycleBinDocumentByIdResponses, DeleteRecycleBinDocumentData, DeleteRecycleBinDocumentErrors, DeleteRecycleBinDocumentResponses, DeleteRecycleBinMediaByIdData, DeleteRecycleBinMediaByIdErrors, DeleteRecycleBinMediaByIdResponses, DeleteRecycleBinMediaData, DeleteRecycleBinMediaErrors, DeleteRecycleBinMediaResponses, DeleteRedirectManagementByIdData, DeleteRedirectManagementByIdErrors, DeleteRedirectManagementByIdResponses, DeleteScriptByPathData, DeleteScriptByPathErrors, DeleteScriptByPathResponses, DeleteScriptFolderByPathData, DeleteScriptFolderByPathErrors, DeleteScriptFolderByPathResponses, DeleteStylesheetByPathData, DeleteStylesheetByPathErrors, DeleteStylesheetByPathResponses, DeleteStylesheetFolderByPathData, DeleteStylesheetFolderByPathErrors, DeleteStylesheetFolderByPathResponses, DeleteTemplateByIdData, DeleteTemplateByIdErrors, DeleteTemplateByIdResponses, DeleteTemporaryFileByIdData, DeleteTemporaryFileByIdErrors, DeleteTemporaryFileByIdResponses, DeleteUserAvatarByIdData, DeleteUserAvatarByIdErrors, DeleteUserAvatarByIdResponses, DeleteUserById2FaByProviderNameData, DeleteUserById2FaByProviderNameErrors, DeleteUserById2FaByProviderNameResponses, DeleteUserByIdClientCredentialsByClientIdData, DeleteUserByIdClientCredentialsByClientIdErrors, DeleteUserByIdClientCredentialsByClientIdResponses, DeleteUserByIdData, DeleteUserByIdErrors, DeleteUserByIdResponses, DeleteUserCurrent2FaByProviderNameData, DeleteUserCurrent2FaByProviderNameErrors, DeleteUserCurrent2FaByProviderNameResponses, DeleteUserData, DeleteUserDataByIdData, DeleteUserDataByIdErrors, DeleteUserDataByIdResponses, DeleteUserErrors, DeleteUserGroupByIdData, DeleteUserGroupByIdErrors, DeleteUserGroupByIdResponses, DeleteUserGroupByIdUsersData, DeleteUserGroupByIdUsersErrors, DeleteUserGroupByIdUsersResponses, DeleteUserGroupData, DeleteUserGroupErrors, DeleteUserGroupResponses, DeleteUserResponses, DeleteWebhookByIdData, DeleteWebhookByIdErrors, DeleteWebhookByIdResponses, GetCollectionDocumentByIdData, GetCollectionDocumentByIdErrors, GetCollectionDocumentByIdResponses, GetCollectionMediaData, GetCollectionMediaErrors, GetCollectionMediaResponses, GetCultureData, GetCultureErrors, GetCultureResponses, GetDataTypeByIdData, GetDataTypeByIdErrors, GetDataTypeByIdIsUsedData, GetDataTypeByIdIsUsedErrors, GetDataTypeByIdIsUsedResponses, GetDataTypeByIdReferencedByData, GetDataTypeByIdReferencedByErrors, GetDataTypeByIdReferencedByResponses, GetDataTypeByIdResponses, GetDataTypeConfigurationData, GetDataTypeConfigurationErrors, GetDataTypeConfigurationResponses, GetDataTypeFolderByIdData, GetDataTypeFolderByIdErrors, GetDataTypeFolderByIdResponses, GetDictionaryByIdData, GetDictionaryByIdErrors, GetDictionaryByIdExportData, GetDictionaryByIdExportErrors, GetDictionaryByIdExportResponses, GetDictionaryByIdResponses, GetDictionaryData, GetDictionaryErrors, GetDictionaryResponses, GetDocumentAreReferencedData, GetDocumentAreReferencedErrors, GetDocumentAreReferencedResponses, GetDocumentBlueprintByIdData, GetDocumentBlueprintByIdErrors, GetDocumentBlueprintByIdResponses, GetDocumentBlueprintByIdScaffoldData, GetDocumentBlueprintByIdScaffoldErrors, GetDocumentBlueprintByIdScaffoldResponses, GetDocumentBlueprintFolderByIdData, GetDocumentBlueprintFolderByIdErrors, GetDocumentBlueprintFolderByIdResponses, GetDocumentByIdAuditLogData, GetDocumentByIdAuditLogErrors, GetDocumentByIdAuditLogResponses, GetDocumentByIdAvailableSegmentOptionsData, GetDocumentByIdAvailableSegmentOptionsErrors, GetDocumentByIdAvailableSegmentOptionsResponses, GetDocumentByIdData, GetDocumentByIdDomainsData, GetDocumentByIdDomainsErrors, GetDocumentByIdDomainsResponses, GetDocumentByIdErrors, GetDocumentByIdNotificationsData, GetDocumentByIdNotificationsErrors, GetDocumentByIdNotificationsResponses, GetDocumentByIdPreviewUrlData, GetDocumentByIdPreviewUrlErrors, GetDocumentByIdPreviewUrlResponses, GetDocumentByIdPublicAccessData, GetDocumentByIdPublicAccessErrors, GetDocumentByIdPublicAccessResponses, GetDocumentByIdPublishedData, GetDocumentByIdPublishedErrors, GetDocumentByIdPublishedResponses, GetDocumentByIdPublishWithDescendantsResultByTaskIdData, GetDocumentByIdPublishWithDescendantsResultByTaskIdErrors, GetDocumentByIdPublishWithDescendantsResultByTaskIdResponses, GetDocumentByIdReferencedByData, GetDocumentByIdReferencedByErrors, GetDocumentByIdReferencedByResponses, GetDocumentByIdReferencedDescendantsData, GetDocumentByIdReferencedDescendantsErrors, GetDocumentByIdReferencedDescendantsResponses, GetDocumentByIdResponses, GetDocumentConfigurationData, GetDocumentConfigurationErrors, GetDocumentConfigurationResponses, GetDocumentTypeAllowedAtRootData, GetDocumentTypeAllowedAtRootErrors, GetDocumentTypeAllowedAtRootResponses, GetDocumentTypeByIdAllowedChildrenData, GetDocumentTypeByIdAllowedChildrenErrors, GetDocumentTypeByIdAllowedChildrenResponses, GetDocumentTypeByIdBlueprintData, GetDocumentTypeByIdBlueprintErrors, GetDocumentTypeByIdBlueprintResponses, GetDocumentTypeByIdCompositionReferencesData, GetDocumentTypeByIdCompositionReferencesErrors, GetDocumentTypeByIdCompositionReferencesResponses, GetDocumentTypeByIdData, GetDocumentTypeByIdErrors, GetDocumentTypeByIdExportData, GetDocumentTypeByIdExportErrors, GetDocumentTypeByIdExportResponses, GetDocumentTypeByIdResponses, GetDocumentTypeConfigurationData, GetDocumentTypeConfigurationErrors, GetDocumentTypeConfigurationResponses, GetDocumentTypeFolderByIdData, GetDocumentTypeFolderByIdErrors, GetDocumentTypeFolderByIdResponses, GetDocumentUrlsData, GetDocumentUrlsErrors, GetDocumentUrlsResponses, GetDocumentVersionByIdData, GetDocumentVersionByIdErrors, GetDocumentVersionByIdResponses, GetDocumentVersionData, GetDocumentVersionErrors, GetDocumentVersionResponses, GetDynamicRootStepsData, GetDynamicRootStepsErrors, GetDynamicRootStepsResponses, GetFilterDataTypeData, GetFilterDataTypeErrors, GetFilterDataTypeResponses, GetFilterMemberData, GetFilterMemberErrors, GetFilterMemberResponses, GetFilterUserData, GetFilterUserErrors, GetFilterUserGroupData, GetFilterUserGroupErrors, GetFilterUserGroupResponses, GetFilterUserResponses, GetHealthCheckGroupByNameData, GetHealthCheckGroupByNameErrors, GetHealthCheckGroupByNameResponses, GetHealthCheckGroupData, GetHealthCheckGroupErrors, GetHealthCheckGroupResponses, GetHelpData, GetHelpErrors, GetHelpResponses, GetImagingResizeUrlsData, GetImagingResizeUrlsErrors, GetImagingResizeUrlsResponses, GetImportAnalyzeData, GetImportAnalyzeErrors, GetImportAnalyzeResponses, GetIndexerByIndexNameData, GetIndexerByIndexNameErrors, GetIndexerByIndexNameResponses, GetIndexerData, GetIndexerErrors, GetIndexerResponses, GetInstallSettingsData, GetInstallSettingsErrors, GetInstallSettingsResponses, GetItemDataTypeData, GetItemDataTypeErrors, GetItemDataTypeResponses, GetItemDataTypeSearchData, GetItemDataTypeSearchErrors, GetItemDataTypeSearchResponses, GetItemDictionaryData, GetItemDictionaryErrors, GetItemDictionaryResponses, GetItemDocumentBlueprintData, GetItemDocumentBlueprintErrors, GetItemDocumentBlueprintResponses, GetItemDocumentData, GetItemDocumentErrors, GetItemDocumentResponses, GetItemDocumentSearchData, GetItemDocumentSearchErrors, GetItemDocumentSearchResponses, GetItemDocumentTypeData, GetItemDocumentTypeErrors, GetItemDocumentTypeResponses, GetItemDocumentTypeSearchData, GetItemDocumentTypeSearchErrors, GetItemDocumentTypeSearchResponses, GetItemLanguageData, GetItemLanguageDefaultData, GetItemLanguageDefaultErrors, GetItemLanguageDefaultResponses, GetItemLanguageErrors, GetItemLanguageResponses, GetItemMediaData, GetItemMediaErrors, GetItemMediaResponses, GetItemMediaSearchData, GetItemMediaSearchErrors, GetItemMediaSearchResponses, GetItemMediaTypeAllowedData, GetItemMediaTypeAllowedErrors, GetItemMediaTypeAllowedResponses, GetItemMediaTypeData, GetItemMediaTypeErrors, GetItemMediaTypeFoldersData, GetItemMediaTypeFoldersErrors, GetItemMediaTypeFoldersResponses, GetItemMediaTypeResponses, GetItemMediaTypeSearchData, GetItemMediaTypeSearchErrors, GetItemMediaTypeSearchResponses, GetItemMemberData, GetItemMemberErrors, GetItemMemberGroupData, GetItemMemberGroupErrors, GetItemMemberGroupResponses, GetItemMemberResponses, GetItemMemberSearchData, GetItemMemberSearchErrors, GetItemMemberSearchResponses, GetItemMemberTypeData, GetItemMemberTypeErrors, GetItemMemberTypeResponses, GetItemMemberTypeSearchData, GetItemMemberTypeSearchErrors, GetItemMemberTypeSearchResponses, GetItemPartialViewData, GetItemPartialViewErrors, GetItemPartialViewResponses, GetItemRelationTypeData, GetItemRelationTypeErrors, GetItemRelationTypeResponses, GetItemScriptData, GetItemScriptErrors, GetItemScriptResponses, GetItemStaticFileData, GetItemStaticFileErrors, GetItemStaticFileResponses, GetItemStylesheetData, GetItemStylesheetErrors, GetItemStylesheetResponses, GetItemTemplateData, GetItemTemplateErrors, GetItemTemplateResponses, GetItemTemplateSearchData, GetItemTemplateSearchErrors, GetItemTemplateSearchResponses, GetItemUserData, GetItemUserErrors, GetItemUserGroupData, GetItemUserGroupErrors, GetItemUserGroupResponses, GetItemUserResponses, GetItemWebhookData, GetItemWebhookErrors, GetItemWebhookResponses, GetLanguageByIsoCodeData, GetLanguageByIsoCodeErrors, GetLanguageByIsoCodeResponses, GetLanguageData, GetLanguageErrors, GetLanguageResponses, GetLogViewerLevelCountData, GetLogViewerLevelCountErrors, GetLogViewerLevelCountResponses, GetLogViewerLevelData, GetLogViewerLevelErrors, GetLogViewerLevelResponses, GetLogViewerLogData, GetLogViewerLogErrors, GetLogViewerLogResponses, GetLogViewerMessageTemplateData, GetLogViewerMessageTemplateErrors, GetLogViewerMessageTemplateResponses, GetLogViewerSavedSearchByNameData, GetLogViewerSavedSearchByNameErrors, GetLogViewerSavedSearchByNameResponses, GetLogViewerSavedSearchData, GetLogViewerSavedSearchErrors, GetLogViewerSavedSearchResponses, GetLogViewerValidateLogsSizeData, GetLogViewerValidateLogsSizeErrors, GetLogViewerValidateLogsSizeResponses, GetManifestManifestData, GetManifestManifestErrors, GetManifestManifestPrivateData, GetManifestManifestPrivateErrors, GetManifestManifestPrivateResponses, GetManifestManifestPublicData, GetManifestManifestPublicResponses, GetManifestManifestResponses, GetMediaAreReferencedData, GetMediaAreReferencedErrors, GetMediaAreReferencedResponses, GetMediaByIdAuditLogData, GetMediaByIdAuditLogErrors, GetMediaByIdAuditLogResponses, GetMediaByIdData, GetMediaByIdErrors, GetMediaByIdReferencedByData, GetMediaByIdReferencedByErrors, GetMediaByIdReferencedByResponses, GetMediaByIdReferencedDescendantsData, GetMediaByIdReferencedDescendantsErrors, GetMediaByIdReferencedDescendantsResponses, GetMediaByIdResponses, GetMediaConfigurationData, GetMediaConfigurationErrors, GetMediaConfigurationResponses, GetMediaTypeAllowedAtRootData, GetMediaTypeAllowedAtRootErrors, GetMediaTypeAllowedAtRootResponses, GetMediaTypeByIdAllowedChildrenData, GetMediaTypeByIdAllowedChildrenErrors, GetMediaTypeByIdAllowedChildrenResponses, GetMediaTypeByIdCompositionReferencesData, GetMediaTypeByIdCompositionReferencesErrors, GetMediaTypeByIdCompositionReferencesResponses, GetMediaTypeByIdData, GetMediaTypeByIdErrors, GetMediaTypeByIdExportData, GetMediaTypeByIdExportErrors, GetMediaTypeByIdExportResponses, GetMediaTypeByIdResponses, GetMediaTypeConfigurationData, GetMediaTypeConfigurationErrors, GetMediaTypeConfigurationResponses, GetMediaTypeFolderByIdData, GetMediaTypeFolderByIdErrors, GetMediaTypeFolderByIdResponses, GetMediaUrlsData, GetMediaUrlsErrors, GetMediaUrlsResponses, GetMemberAreReferencedData, GetMemberAreReferencedErrors, GetMemberAreReferencedResponses, GetMemberByIdData, GetMemberByIdErrors, GetMemberByIdReferencedByData, GetMemberByIdReferencedByErrors, GetMemberByIdReferencedByResponses, GetMemberByIdReferencedDescendantsData, GetMemberByIdReferencedDescendantsErrors, GetMemberByIdReferencedDescendantsResponses, GetMemberByIdResponses, GetMemberConfigurationData, GetMemberConfigurationErrors, GetMemberConfigurationResponses, GetMemberGroupByIdData, GetMemberGroupByIdErrors, GetMemberGroupByIdResponses, GetMemberGroupData, GetMemberGroupErrors, GetMemberGroupResponses, GetMemberTypeByIdCompositionReferencesData, GetMemberTypeByIdCompositionReferencesErrors, GetMemberTypeByIdCompositionReferencesResponses, GetMemberTypeByIdData, GetMemberTypeByIdErrors, GetMemberTypeByIdResponses, GetMemberTypeConfigurationData, GetMemberTypeConfigurationErrors, GetMemberTypeConfigurationResponses, GetMemberTypeFolderByIdData, GetMemberTypeFolderByIdErrors, GetMemberTypeFolderByIdResponses, GetModelsBuilderDashboardData, GetModelsBuilderDashboardErrors, GetModelsBuilderDashboardResponses, GetModelsBuilderStatusData, GetModelsBuilderStatusErrors, GetModelsBuilderStatusResponses, GetNewsDashboardData, GetNewsDashboardErrors, GetNewsDashboardResponses, GetObjectTypesData, GetObjectTypesErrors, GetObjectTypesResponses, GetOembedQueryData, GetOembedQueryErrors, GetOembedQueryResponses, GetPackageConfigurationData, GetPackageConfigurationErrors, GetPackageConfigurationResponses, GetPackageCreatedByIdData, GetPackageCreatedByIdDownloadData, GetPackageCreatedByIdDownloadErrors, GetPackageCreatedByIdDownloadResponses, GetPackageCreatedByIdErrors, GetPackageCreatedByIdResponses, GetPackageCreatedData, GetPackageCreatedErrors, GetPackageCreatedResponses, GetPackageMigrationStatusData, GetPackageMigrationStatusErrors, GetPackageMigrationStatusResponses, GetPartialViewByPathData, GetPartialViewByPathErrors, GetPartialViewByPathResponses, GetPartialViewFolderByPathData, GetPartialViewFolderByPathErrors, GetPartialViewFolderByPathResponses, GetPartialViewSnippetByIdData, GetPartialViewSnippetByIdErrors, GetPartialViewSnippetByIdResponses, GetPartialViewSnippetData, GetPartialViewSnippetErrors, GetPartialViewSnippetResponses, GetProfilingStatusData, GetProfilingStatusErrors, GetProfilingStatusResponses, GetPropertyTypeIsUsedData, GetPropertyTypeIsUsedErrors, GetPropertyTypeIsUsedResponses, GetPublishedCacheRebuildStatusData, GetPublishedCacheRebuildStatusErrors, GetPublishedCacheRebuildStatusResponses, GetRecycleBinDocumentByIdOriginalParentData, GetRecycleBinDocumentByIdOriginalParentErrors, GetRecycleBinDocumentByIdOriginalParentResponses, GetRecycleBinDocumentChildrenData, GetRecycleBinDocumentChildrenErrors, GetRecycleBinDocumentChildrenResponses, GetRecycleBinDocumentReferencedByData, GetRecycleBinDocumentReferencedByErrors, GetRecycleBinDocumentReferencedByResponses, GetRecycleBinDocumentRootData, GetRecycleBinDocumentRootErrors, GetRecycleBinDocumentRootResponses, GetRecycleBinDocumentSiblingsData, GetRecycleBinDocumentSiblingsErrors, GetRecycleBinDocumentSiblingsResponses, GetRecycleBinMediaByIdOriginalParentData, GetRecycleBinMediaByIdOriginalParentErrors, GetRecycleBinMediaByIdOriginalParentResponses, GetRecycleBinMediaChildrenData, GetRecycleBinMediaChildrenErrors, GetRecycleBinMediaChildrenResponses, GetRecycleBinMediaReferencedByData, GetRecycleBinMediaReferencedByErrors, GetRecycleBinMediaReferencedByResponses, GetRecycleBinMediaRootData, GetRecycleBinMediaRootErrors, GetRecycleBinMediaRootResponses, GetRecycleBinMediaSiblingsData, GetRecycleBinMediaSiblingsErrors, GetRecycleBinMediaSiblingsResponses, GetRedirectManagementByIdData, GetRedirectManagementByIdErrors, GetRedirectManagementByIdResponses, GetRedirectManagementData, GetRedirectManagementErrors, GetRedirectManagementResponses, GetRedirectManagementStatusData, GetRedirectManagementStatusErrors, GetRedirectManagementStatusResponses, GetRelationByRelationTypeIdData, GetRelationByRelationTypeIdErrors, GetRelationByRelationTypeIdResponses, GetRelationTypeByIdData, GetRelationTypeByIdErrors, GetRelationTypeByIdResponses, GetRelationTypeData, GetRelationTypeErrors, GetRelationTypeResponses, GetScriptByPathData, GetScriptByPathErrors, GetScriptByPathResponses, GetScriptFolderByPathData, GetScriptFolderByPathErrors, GetScriptFolderByPathResponses, GetSearcherBySearcherNameQueryData, GetSearcherBySearcherNameQueryErrors, GetSearcherBySearcherNameQueryResponses, GetSearcherData, GetSearcherErrors, GetSearcherResponses, GetSecurityConfigurationData, GetSecurityConfigurationErrors, GetSecurityConfigurationResponses, GetSegmentData, GetSegmentErrors, GetSegmentResponses, GetServerConfigurationData, GetServerConfigurationResponses, GetServerInformationData, GetServerInformationErrors, GetServerInformationResponses, GetServerStatusData, GetServerStatusErrors, GetServerStatusResponses, GetServerTroubleshootingData, GetServerTroubleshootingErrors, GetServerTroubleshootingResponses, GetServerUpgradeCheckData, GetServerUpgradeCheckErrors, GetServerUpgradeCheckResponses, GetStylesheetByPathData, GetStylesheetByPathErrors, GetStylesheetByPathResponses, GetStylesheetFolderByPathData, GetStylesheetFolderByPathErrors, GetStylesheetFolderByPathResponses, GetTagData, GetTagErrors, GetTagResponses, GetTelemetryData, GetTelemetryErrors, GetTelemetryLevelData, GetTelemetryLevelErrors, GetTelemetryLevelResponses, GetTelemetryResponses, GetTemplateByIdData, GetTemplateByIdErrors, GetTemplateByIdResponses, GetTemplateConfigurationData, GetTemplateConfigurationErrors, GetTemplateConfigurationResponses, GetTemplateQuerySettingsData, GetTemplateQuerySettingsErrors, GetTemplateQuerySettingsResponses, GetTemporaryFileByIdData, GetTemporaryFileByIdErrors, GetTemporaryFileByIdResponses, GetTemporaryFileConfigurationData, GetTemporaryFileConfigurationErrors, GetTemporaryFileConfigurationResponses, GetTreeDataTypeAncestorsData, GetTreeDataTypeAncestorsErrors, GetTreeDataTypeAncestorsResponses, GetTreeDataTypeChildrenData, GetTreeDataTypeChildrenErrors, GetTreeDataTypeChildrenResponses, GetTreeDataTypeRootData, GetTreeDataTypeRootErrors, GetTreeDataTypeRootResponses, GetTreeDataTypeSiblingsData, GetTreeDataTypeSiblingsErrors, GetTreeDataTypeSiblingsResponses, GetTreeDictionaryAncestorsData, GetTreeDictionaryAncestorsErrors, GetTreeDictionaryAncestorsResponses, GetTreeDictionaryChildrenData, GetTreeDictionaryChildrenErrors, GetTreeDictionaryChildrenResponses, GetTreeDictionaryRootData, GetTreeDictionaryRootErrors, GetTreeDictionaryRootResponses, GetTreeDocumentAncestorsData, GetTreeDocumentAncestorsErrors, GetTreeDocumentAncestorsResponses, GetTreeDocumentBlueprintAncestorsData, GetTreeDocumentBlueprintAncestorsErrors, GetTreeDocumentBlueprintAncestorsResponses, GetTreeDocumentBlueprintChildrenData, GetTreeDocumentBlueprintChildrenErrors, GetTreeDocumentBlueprintChildrenResponses, GetTreeDocumentBlueprintRootData, GetTreeDocumentBlueprintRootErrors, GetTreeDocumentBlueprintRootResponses, GetTreeDocumentBlueprintSiblingsData, GetTreeDocumentBlueprintSiblingsErrors, GetTreeDocumentBlueprintSiblingsResponses, GetTreeDocumentChildrenData, GetTreeDocumentChildrenErrors, GetTreeDocumentChildrenResponses, GetTreeDocumentRootData, GetTreeDocumentRootErrors, GetTreeDocumentRootResponses, GetTreeDocumentSiblingsData, GetTreeDocumentSiblingsErrors, GetTreeDocumentSiblingsResponses, GetTreeDocumentTypeAncestorsData, GetTreeDocumentTypeAncestorsErrors, GetTreeDocumentTypeAncestorsResponses, GetTreeDocumentTypeChildrenData, GetTreeDocumentTypeChildrenErrors, GetTreeDocumentTypeChildrenResponses, GetTreeDocumentTypeRootData, GetTreeDocumentTypeRootErrors, GetTreeDocumentTypeRootResponses, GetTreeDocumentTypeSiblingsData, GetTreeDocumentTypeSiblingsErrors, GetTreeDocumentTypeSiblingsResponses, GetTreeMediaAncestorsData, GetTreeMediaAncestorsErrors, GetTreeMediaAncestorsResponses, GetTreeMediaChildrenData, GetTreeMediaChildrenErrors, GetTreeMediaChildrenResponses, GetTreeMediaRootData, GetTreeMediaRootErrors, GetTreeMediaRootResponses, GetTreeMediaSiblingsData, GetTreeMediaSiblingsErrors, GetTreeMediaSiblingsResponses, GetTreeMediaTypeAncestorsData, GetTreeMediaTypeAncestorsErrors, GetTreeMediaTypeAncestorsResponses, GetTreeMediaTypeChildrenData, GetTreeMediaTypeChildrenErrors, GetTreeMediaTypeChildrenResponses, GetTreeMediaTypeRootData, GetTreeMediaTypeRootErrors, GetTreeMediaTypeRootResponses, GetTreeMediaTypeSiblingsData, GetTreeMediaTypeSiblingsErrors, GetTreeMediaTypeSiblingsResponses, GetTreeMemberGroupRootData, GetTreeMemberGroupRootErrors, GetTreeMemberGroupRootResponses, GetTreeMemberTypeRootData, GetTreeMemberTypeRootErrors, GetTreeMemberTypeRootResponses, GetTreeMemberTypeSiblingsData, GetTreeMemberTypeSiblingsErrors, GetTreeMemberTypeSiblingsResponses, GetTreePartialViewAncestorsData, GetTreePartialViewAncestorsErrors, GetTreePartialViewAncestorsResponses, GetTreePartialViewChildrenData, GetTreePartialViewChildrenErrors, GetTreePartialViewChildrenResponses, GetTreePartialViewRootData, GetTreePartialViewRootErrors, GetTreePartialViewRootResponses, GetTreePartialViewSiblingsData, GetTreePartialViewSiblingsErrors, GetTreePartialViewSiblingsResponses, GetTreeScriptAncestorsData, GetTreeScriptAncestorsErrors, GetTreeScriptAncestorsResponses, GetTreeScriptChildrenData, GetTreeScriptChildrenErrors, GetTreeScriptChildrenResponses, GetTreeScriptRootData, GetTreeScriptRootErrors, GetTreeScriptRootResponses, GetTreeScriptSiblingsData, GetTreeScriptSiblingsErrors, GetTreeScriptSiblingsResponses, GetTreeStaticFileAncestorsData, GetTreeStaticFileAncestorsErrors, GetTreeStaticFileAncestorsResponses, GetTreeStaticFileChildrenData, GetTreeStaticFileChildrenErrors, GetTreeStaticFileChildrenResponses, GetTreeStaticFileRootData, GetTreeStaticFileRootErrors, GetTreeStaticFileRootResponses, GetTreeStylesheetAncestorsData, GetTreeStylesheetAncestorsErrors, GetTreeStylesheetAncestorsResponses, GetTreeStylesheetChildrenData, GetTreeStylesheetChildrenErrors, GetTreeStylesheetChildrenResponses, GetTreeStylesheetRootData, GetTreeStylesheetRootErrors, GetTreeStylesheetRootResponses, GetTreeStylesheetSiblingsData, GetTreeStylesheetSiblingsErrors, GetTreeStylesheetSiblingsResponses, GetTreeTemplateAncestorsData, GetTreeTemplateAncestorsErrors, GetTreeTemplateAncestorsResponses, GetTreeTemplateChildrenData, GetTreeTemplateChildrenErrors, GetTreeTemplateChildrenResponses, GetTreeTemplateRootData, GetTreeTemplateRootErrors, GetTreeTemplateRootResponses, GetTreeTemplateSiblingsData, GetTreeTemplateSiblingsErrors, GetTreeTemplateSiblingsResponses, GetUpgradeSettingsData, GetUpgradeSettingsErrors, GetUpgradeSettingsResponses, GetUserById2FaData, GetUserById2FaErrors, GetUserById2FaResponses, GetUserByIdCalculateStartNodesData, GetUserByIdCalculateStartNodesErrors, GetUserByIdCalculateStartNodesResponses, GetUserByIdClientCredentialsData, GetUserByIdClientCredentialsErrors, GetUserByIdClientCredentialsResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, GetUserConfigurationData, GetUserConfigurationErrors, GetUserConfigurationResponses, GetUserCurrent2FaByProviderNameData, GetUserCurrent2FaByProviderNameErrors, GetUserCurrent2FaByProviderNameResponses, GetUserCurrent2FaData, GetUserCurrent2FaErrors, GetUserCurrent2FaResponses, GetUserCurrentConfigurationData, GetUserCurrentConfigurationErrors, GetUserCurrentConfigurationResponses, GetUserCurrentData, GetUserCurrentErrors, GetUserCurrentLoginProvidersData, GetUserCurrentLoginProvidersErrors, GetUserCurrentLoginProvidersResponses, GetUserCurrentPermissionsData, GetUserCurrentPermissionsDocumentData, GetUserCurrentPermissionsDocumentErrors, GetUserCurrentPermissionsDocumentResponses, GetUserCurrentPermissionsErrors, GetUserCurrentPermissionsMediaData, GetUserCurrentPermissionsMediaErrors, GetUserCurrentPermissionsMediaResponses, GetUserCurrentPermissionsResponses, GetUserCurrentResponses, GetUserData, GetUserDataByIdData, GetUserDataByIdErrors, GetUserDataByIdResponses, GetUserDataData, GetUserDataErrors, GetUserDataResponses, GetUserErrors, GetUserGroupByIdData, GetUserGroupByIdErrors, GetUserGroupByIdResponses, GetUserGroupData, GetUserGroupErrors, GetUserGroupResponses, GetUserResponses, GetWebhookByIdData, GetWebhookByIdErrors, GetWebhookByIdLogsData, GetWebhookByIdLogsErrors, GetWebhookByIdLogsResponses, GetWebhookByIdResponses, GetWebhookData, GetWebhookErrors, GetWebhookEventsData, GetWebhookEventsErrors, GetWebhookEventsResponses, GetWebhookLogsData, GetWebhookLogsErrors, GetWebhookLogsResponses, GetWebhookResponses, PostDataTypeByIdCopyData, PostDataTypeByIdCopyErrors, PostDataTypeByIdCopyResponses, PostDataTypeData, PostDataTypeErrors, PostDataTypeFolderData, PostDataTypeFolderErrors, PostDataTypeFolderResponses, PostDataTypeResponses, PostDictionaryData, PostDictionaryErrors, PostDictionaryImportData, PostDictionaryImportErrors, PostDictionaryImportResponses, PostDictionaryResponses, PostDocumentBlueprintData, PostDocumentBlueprintErrors, PostDocumentBlueprintFolderData, PostDocumentBlueprintFolderErrors, PostDocumentBlueprintFolderResponses, PostDocumentBlueprintFromDocumentData, PostDocumentBlueprintFromDocumentErrors, PostDocumentBlueprintFromDocumentResponses, PostDocumentBlueprintResponses, PostDocumentByIdCopyData, PostDocumentByIdCopyErrors, PostDocumentByIdCopyResponses, PostDocumentByIdPublicAccessData, PostDocumentByIdPublicAccessErrors, PostDocumentByIdPublicAccessResponses, PostDocumentData, PostDocumentErrors, PostDocumentResponses, PostDocumentTypeAvailableCompositionsData, PostDocumentTypeAvailableCompositionsErrors, PostDocumentTypeAvailableCompositionsResponses, PostDocumentTypeByIdCopyData, PostDocumentTypeByIdCopyErrors, PostDocumentTypeByIdCopyResponses, PostDocumentTypeData, PostDocumentTypeErrors, PostDocumentTypeFolderData, PostDocumentTypeFolderErrors, PostDocumentTypeFolderResponses, PostDocumentTypeImportData, PostDocumentTypeImportErrors, PostDocumentTypeImportResponses, PostDocumentTypeResponses, PostDocumentValidateData, PostDocumentValidateErrors, PostDocumentValidateResponses, PostDocumentVersionByIdRollbackData, PostDocumentVersionByIdRollbackErrors, PostDocumentVersionByIdRollbackResponses, PostDynamicRootQueryData, PostDynamicRootQueryErrors, PostDynamicRootQueryResponses, PostHealthCheckExecuteActionData, PostHealthCheckExecuteActionErrors, PostHealthCheckExecuteActionResponses, PostHealthCheckGroupByNameCheckData, PostHealthCheckGroupByNameCheckErrors, PostHealthCheckGroupByNameCheckResponses, PostIndexerByIndexNameRebuildData, PostIndexerByIndexNameRebuildErrors, PostIndexerByIndexNameRebuildResponses, PostInstallSetupData, PostInstallSetupErrors, PostInstallSetupResponses, PostInstallValidateDatabaseData, PostInstallValidateDatabaseErrors, PostInstallValidateDatabaseResponses, PostLanguageData, PostLanguageErrors, PostLanguageResponses, PostLogViewerSavedSearchData, PostLogViewerSavedSearchErrors, PostLogViewerSavedSearchResponses, PostMediaData, PostMediaErrors, PostMediaResponses, PostMediaTypeAvailableCompositionsData, PostMediaTypeAvailableCompositionsErrors, PostMediaTypeAvailableCompositionsResponses, PostMediaTypeByIdCopyData, PostMediaTypeByIdCopyErrors, PostMediaTypeByIdCopyResponses, PostMediaTypeData, PostMediaTypeErrors, PostMediaTypeFolderData, PostMediaTypeFolderErrors, PostMediaTypeFolderResponses, PostMediaTypeImportData, PostMediaTypeImportErrors, PostMediaTypeImportResponses, PostMediaTypeResponses, PostMediaValidateData, PostMediaValidateErrors, PostMediaValidateResponses, PostMemberData, PostMemberErrors, PostMemberGroupData, PostMemberGroupErrors, PostMemberGroupResponses, PostMemberResponses, PostMemberTypeAvailableCompositionsData, PostMemberTypeAvailableCompositionsErrors, PostMemberTypeAvailableCompositionsResponses, PostMemberTypeByIdCopyData, PostMemberTypeByIdCopyErrors, PostMemberTypeByIdCopyResponses, PostMemberTypeData, PostMemberTypeErrors, PostMemberTypeFolderData, PostMemberTypeFolderErrors, PostMemberTypeFolderResponses, PostMemberTypeResponses, PostMemberValidateData, PostMemberValidateErrors, PostMemberValidateResponses, PostModelsBuilderBuildData, PostModelsBuilderBuildErrors, PostModelsBuilderBuildResponses, PostPackageByNameRunMigrationData, PostPackageByNameRunMigrationErrors, PostPackageByNameRunMigrationResponses, PostPackageCreatedData, PostPackageCreatedErrors, PostPackageCreatedResponses, PostPartialViewData, PostPartialViewErrors, PostPartialViewFolderData, PostPartialViewFolderErrors, PostPartialViewFolderResponses, PostPartialViewResponses, PostPreviewData, PostPreviewErrors, PostPreviewResponses, PostPublishedCacheRebuildData, PostPublishedCacheRebuildErrors, PostPublishedCacheRebuildResponses, PostPublishedCacheReloadData, PostPublishedCacheReloadErrors, PostPublishedCacheReloadResponses, PostRedirectManagementStatusData, PostRedirectManagementStatusErrors, PostRedirectManagementStatusResponses, PostScriptData, PostScriptErrors, PostScriptFolderData, PostScriptFolderErrors, PostScriptFolderResponses, PostScriptResponses, PostSecurityForgotPasswordData, PostSecurityForgotPasswordErrors, PostSecurityForgotPasswordResetData, PostSecurityForgotPasswordResetErrors, PostSecurityForgotPasswordResetResponses, PostSecurityForgotPasswordResponses, PostSecurityForgotPasswordVerifyData, PostSecurityForgotPasswordVerifyErrors, PostSecurityForgotPasswordVerifyResponses, PostStylesheetData, PostStylesheetErrors, PostStylesheetFolderData, PostStylesheetFolderErrors, PostStylesheetFolderResponses, PostStylesheetResponses, PostTelemetryLevelData, PostTelemetryLevelErrors, PostTelemetryLevelResponses, PostTemplateData, PostTemplateErrors, PostTemplateQueryExecuteData, PostTemplateQueryExecuteErrors, PostTemplateQueryExecuteResponses, PostTemplateResponses, PostTemporaryFileData, PostTemporaryFileErrors, PostTemporaryFileResponses, PostUpgradeAuthorizeData, PostUpgradeAuthorizeErrors, PostUpgradeAuthorizeResponses, PostUserAvatarByIdData, PostUserAvatarByIdErrors, PostUserAvatarByIdResponses, PostUserByIdChangePasswordData, PostUserByIdChangePasswordErrors, PostUserByIdChangePasswordResponses, PostUserByIdClientCredentialsData, PostUserByIdClientCredentialsErrors, PostUserByIdClientCredentialsResponses, PostUserByIdResetPasswordData, PostUserByIdResetPasswordErrors, PostUserByIdResetPasswordResponses, PostUserCurrent2FaByProviderNameData, PostUserCurrent2FaByProviderNameErrors, PostUserCurrent2FaByProviderNameResponses, PostUserCurrentAvatarData, PostUserCurrentAvatarErrors, PostUserCurrentAvatarResponses, PostUserCurrentChangePasswordData, PostUserCurrentChangePasswordErrors, PostUserCurrentChangePasswordResponses, PostUserData, PostUserDataData, PostUserDataErrors, PostUserDataResponses, PostUserDisableData, PostUserDisableErrors, PostUserDisableResponses, PostUserEnableData, PostUserEnableErrors, PostUserEnableResponses, PostUserErrors, PostUserGroupByIdUsersData, PostUserGroupByIdUsersErrors, PostUserGroupByIdUsersResponses, PostUserGroupData, PostUserGroupErrors, PostUserGroupResponses, PostUserInviteCreatePasswordData, PostUserInviteCreatePasswordErrors, PostUserInviteCreatePasswordResponses, PostUserInviteData, PostUserInviteErrors, PostUserInviteResendData, PostUserInviteResendErrors, PostUserInviteResendResponses, PostUserInviteResponses, PostUserInviteVerifyData, PostUserInviteVerifyErrors, PostUserInviteVerifyResponses, PostUserResponses, PostUserSetUserGroupsData, PostUserSetUserGroupsErrors, PostUserSetUserGroupsResponses, PostUserUnlockData, PostUserUnlockErrors, PostUserUnlockResponses, PostWebhookData, PostWebhookErrors, PostWebhookResponses, PutDataTypeByIdData, PutDataTypeByIdErrors, PutDataTypeByIdMoveData, PutDataTypeByIdMoveErrors, PutDataTypeByIdMoveResponses, PutDataTypeByIdResponses, PutDataTypeFolderByIdData, PutDataTypeFolderByIdErrors, PutDataTypeFolderByIdResponses, PutDictionaryByIdData, PutDictionaryByIdErrors, PutDictionaryByIdMoveData, PutDictionaryByIdMoveErrors, PutDictionaryByIdMoveResponses, PutDictionaryByIdResponses, PutDocumentBlueprintByIdData, PutDocumentBlueprintByIdErrors, PutDocumentBlueprintByIdMoveData, PutDocumentBlueprintByIdMoveErrors, PutDocumentBlueprintByIdMoveResponses, PutDocumentBlueprintByIdResponses, PutDocumentBlueprintFolderByIdData, PutDocumentBlueprintFolderByIdErrors, PutDocumentBlueprintFolderByIdResponses, PutDocumentByIdData, PutDocumentByIdDomainsData, PutDocumentByIdDomainsErrors, PutDocumentByIdDomainsResponses, PutDocumentByIdErrors, PutDocumentByIdMoveData, PutDocumentByIdMoveErrors, PutDocumentByIdMoveResponses, PutDocumentByIdMoveToRecycleBinData, PutDocumentByIdMoveToRecycleBinErrors, PutDocumentByIdMoveToRecycleBinResponses, PutDocumentByIdNotificationsData, PutDocumentByIdNotificationsErrors, PutDocumentByIdNotificationsResponses, PutDocumentByIdPublicAccessData, PutDocumentByIdPublicAccessErrors, PutDocumentByIdPublicAccessResponses, PutDocumentByIdPublishData, PutDocumentByIdPublishErrors, PutDocumentByIdPublishResponses, PutDocumentByIdPublishWithDescendantsData, PutDocumentByIdPublishWithDescendantsErrors, PutDocumentByIdPublishWithDescendantsResponses, PutDocumentByIdResponses, PutDocumentByIdUnpublishData, PutDocumentByIdUnpublishErrors, PutDocumentByIdUnpublishResponses, PutDocumentSortData, PutDocumentSortErrors, PutDocumentSortResponses, PutDocumentTypeByIdData, PutDocumentTypeByIdErrors, PutDocumentTypeByIdImportData, PutDocumentTypeByIdImportErrors, PutDocumentTypeByIdImportResponses, PutDocumentTypeByIdMoveData, PutDocumentTypeByIdMoveErrors, PutDocumentTypeByIdMoveResponses, PutDocumentTypeByIdResponses, PutDocumentTypeFolderByIdData, PutDocumentTypeFolderByIdErrors, PutDocumentTypeFolderByIdResponses, PutDocumentVersionByIdPreventCleanupData, PutDocumentVersionByIdPreventCleanupErrors, PutDocumentVersionByIdPreventCleanupResponses, PutLanguageByIsoCodeData, PutLanguageByIsoCodeErrors, PutLanguageByIsoCodeResponses, PutMediaByIdData, PutMediaByIdErrors, PutMediaByIdMoveData, PutMediaByIdMoveErrors, PutMediaByIdMoveResponses, PutMediaByIdMoveToRecycleBinData, PutMediaByIdMoveToRecycleBinErrors, PutMediaByIdMoveToRecycleBinResponses, PutMediaByIdResponses, PutMediaByIdValidateData, PutMediaByIdValidateErrors, PutMediaByIdValidateResponses, PutMediaSortData, PutMediaSortErrors, PutMediaSortResponses, PutMediaTypeByIdData, PutMediaTypeByIdErrors, PutMediaTypeByIdImportData, PutMediaTypeByIdImportErrors, PutMediaTypeByIdImportResponses, PutMediaTypeByIdMoveData, PutMediaTypeByIdMoveErrors, PutMediaTypeByIdMoveResponses, PutMediaTypeByIdResponses, PutMediaTypeFolderByIdData, PutMediaTypeFolderByIdErrors, PutMediaTypeFolderByIdResponses, PutMemberByIdData, PutMemberByIdErrors, PutMemberByIdResponses, PutMemberByIdValidateData, PutMemberByIdValidateErrors, PutMemberByIdValidateResponses, PutMemberGroupByIdData, PutMemberGroupByIdErrors, PutMemberGroupByIdResponses, PutMemberTypeByIdData, PutMemberTypeByIdErrors, PutMemberTypeByIdResponses, PutMemberTypeFolderByIdData, PutMemberTypeFolderByIdErrors, PutMemberTypeFolderByIdResponses, PutPackageCreatedByIdData, PutPackageCreatedByIdErrors, PutPackageCreatedByIdResponses, PutPartialViewByPathData, PutPartialViewByPathErrors, PutPartialViewByPathRenameData, PutPartialViewByPathRenameErrors, PutPartialViewByPathRenameResponses, PutPartialViewByPathResponses, PutProfilingStatusData, PutProfilingStatusErrors, PutProfilingStatusResponses, PutRecycleBinDocumentByIdRestoreData, PutRecycleBinDocumentByIdRestoreErrors, PutRecycleBinDocumentByIdRestoreResponses, PutRecycleBinMediaByIdRestoreData, PutRecycleBinMediaByIdRestoreErrors, PutRecycleBinMediaByIdRestoreResponses, PutScriptByPathData, PutScriptByPathErrors, PutScriptByPathRenameData, PutScriptByPathRenameErrors, PutScriptByPathRenameResponses, PutScriptByPathResponses, PutStylesheetByPathData, PutStylesheetByPathErrors, PutStylesheetByPathRenameData, PutStylesheetByPathRenameErrors, PutStylesheetByPathRenameResponses, PutStylesheetByPathResponses, PutTemplateByIdData, PutTemplateByIdErrors, PutTemplateByIdResponses, PutUmbracoManagementApiV11DocumentByIdValidate11Data, PutUmbracoManagementApiV11DocumentByIdValidate11Errors, PutUmbracoManagementApiV11DocumentByIdValidate11Responses, PutUserByIdData, PutUserByIdErrors, PutUserByIdResponses, PutUserDataData, PutUserDataErrors, PutUserDataResponses, PutUserGroupByIdData, PutUserGroupByIdErrors, PutUserGroupByIdResponses, PutWebhookByIdData, PutWebhookByIdErrors, PutWebhookByIdResponses } from './types.gen'; export type Options = Options2 & { /** @@ -3323,6 +3323,66 @@ export class MemberTypeService { }); } + public static postMemberTypeFolder(options?: Options) { + return (options?.client ?? client).post({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/management/api/v1/member-type/folder', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }); + } + + public static deleteMemberTypeFolderById(options: Options) { + return (options.client ?? client).delete({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/management/api/v1/member-type/folder/{id}', + ...options + }); + } + + public static getMemberTypeFolderById(options: Options) { + return (options.client ?? client).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/management/api/v1/member-type/folder/{id}', + ...options + }); + } + + public static putMemberTypeFolderById(options: Options) { + return (options.client ?? client).put({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/management/api/v1/member-type/folder/{id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + } + public static getTreeMemberTypeRoot(options?: Options) { return (options?.client ?? client).get({ security: [ @@ -3578,6 +3638,21 @@ export class ModelsBuilderService { } } +export class NewsDashboardService { + public static getNewsDashboard(options?: Options) { + return (options?.client ?? client).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/management/api/v1/news-dashboard', + ...options + }); + } +} + export class ObjectTypesService { public static getObjectTypes(options?: Options) { return (options?.client ?? client).get({ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts index 97294e9a0bee..0bd3f90ea53e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts @@ -412,7 +412,7 @@ export type CreateUserGroupRequestModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; id?: string | null; }; @@ -471,7 +471,7 @@ export type CurrentUserResponseModel = { hasAccessToAllLanguages: boolean; hasAccessToSensitiveData: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; allowedSections: Array; isAdmin: boolean; }; @@ -642,6 +642,9 @@ export type DocumentConfigurationResponseModel = { disableDeleteWhenReferenced: boolean; disableUnpublishWhenReferenced: boolean; allowEditInvariantFromNonDefault: boolean; + /** + * @deprecated + */ allowNonExistingSegmentsCreation: boolean; }; @@ -770,6 +773,12 @@ export type DocumentTypeItemResponseModel = { description?: string | null; }; +export type DocumentTypePermissionPresentationModel = { + $type: string; + verbs: Array; + documentTypeAlias: string; +}; + export type DocumentTypePropertyTypeContainerResponseModel = { id: string; parent?: ReferenceByIdModel | null; @@ -1634,6 +1643,20 @@ export type NamedEntityTreeItemResponseModel = { name: string; }; +export type NewsDashboardItemResponseModel = { + priority: string; + header: string; + body?: string | null; + buttonText?: string | null; + imageUrl?: string | null; + imageAltText?: string | null; + url?: string | null; +}; + +export type NewsDashboardResponseModel = { + items: Array; +}; + export type NoopSetupTwoFactorModel = { [key: string]: never; }; @@ -2788,7 +2811,7 @@ export type UpdateUserGroupRequestModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; }; export type UpdateUserGroupsOnUserRequestModel = { @@ -2889,7 +2912,7 @@ export type UserGroupResponseModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; id: string; isDeletable: boolean; aliasCanBeChanged: boolean; @@ -10784,6 +10807,150 @@ export type GetMemberTypeConfigurationResponses = { export type GetMemberTypeConfigurationResponse = GetMemberTypeConfigurationResponses[keyof GetMemberTypeConfigurationResponses]; +export type PostMemberTypeFolderData = { + body?: CreateFolderRequestModel; + path?: never; + query?: never; + url: '/umbraco/management/api/v1/member-type/folder'; +}; + +export type PostMemberTypeFolderErrors = { + /** + * Bad Request + */ + 400: ProblemDetails; + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; + /** + * The authenticated user does not have access to this resource + */ + 403: unknown; + /** + * Not Found + */ + 404: ProblemDetails; +}; + +export type PostMemberTypeFolderError = PostMemberTypeFolderErrors[keyof PostMemberTypeFolderErrors]; + +export type PostMemberTypeFolderResponses = { + /** + * Created + */ + 201: unknown; +}; + +export type DeleteMemberTypeFolderByIdData = { + body?: never; + path: { + id: string; + }; + query?: never; + url: '/umbraco/management/api/v1/member-type/folder/{id}'; +}; + +export type DeleteMemberTypeFolderByIdErrors = { + /** + * Bad Request + */ + 400: ProblemDetails; + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; + /** + * The authenticated user does not have access to this resource + */ + 403: unknown; + /** + * Not Found + */ + 404: ProblemDetails; +}; + +export type DeleteMemberTypeFolderByIdError = DeleteMemberTypeFolderByIdErrors[keyof DeleteMemberTypeFolderByIdErrors]; + +export type DeleteMemberTypeFolderByIdResponses = { + /** + * OK + */ + 200: unknown; +}; + +export type GetMemberTypeFolderByIdData = { + body?: never; + path: { + id: string; + }; + query?: never; + url: '/umbraco/management/api/v1/member-type/folder/{id}'; +}; + +export type GetMemberTypeFolderByIdErrors = { + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; + /** + * The authenticated user does not have access to this resource + */ + 403: unknown; + /** + * Not Found + */ + 404: ProblemDetails; +}; + +export type GetMemberTypeFolderByIdError = GetMemberTypeFolderByIdErrors[keyof GetMemberTypeFolderByIdErrors]; + +export type GetMemberTypeFolderByIdResponses = { + /** + * OK + */ + 200: FolderResponseModel; +}; + +export type GetMemberTypeFolderByIdResponse = GetMemberTypeFolderByIdResponses[keyof GetMemberTypeFolderByIdResponses]; + +export type PutMemberTypeFolderByIdData = { + body?: UpdateFolderResponseModel; + path: { + id: string; + }; + query?: never; + url: '/umbraco/management/api/v1/member-type/folder/{id}'; +}; + +export type PutMemberTypeFolderByIdErrors = { + /** + * Bad Request + */ + 400: ProblemDetails; + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; + /** + * The authenticated user does not have access to this resource + */ + 403: unknown; + /** + * Not Found + */ + 404: ProblemDetails; +}; + +export type PutMemberTypeFolderByIdError = PutMemberTypeFolderByIdErrors[keyof PutMemberTypeFolderByIdErrors]; + +export type PutMemberTypeFolderByIdResponses = { + /** + * OK + */ + 200: unknown; +}; + export type GetTreeMemberTypeRootData = { body?: never; path?: never; @@ -11364,6 +11531,29 @@ export type GetModelsBuilderStatusResponses = { export type GetModelsBuilderStatusResponse = GetModelsBuilderStatusResponses[keyof GetModelsBuilderStatusResponses]; +export type GetNewsDashboardData = { + body?: never; + path?: never; + query?: never; + url: '/umbraco/management/api/v1/news-dashboard'; +}; + +export type GetNewsDashboardErrors = { + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; +}; + +export type GetNewsDashboardResponses = { + /** + * OK + */ + 200: NewsDashboardResponseModel; +}; + +export type GetNewsDashboardResponse = GetNewsDashboardResponses[keyof GetNewsDashboardResponses]; + export type GetObjectTypesData = { body?: never; path?: never; diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/index.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/index.ts new file mode 100644 index 000000000000..23a384a46005 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/index.ts @@ -0,0 +1,3 @@ +export * from './repository/index.js'; + +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/index.ts new file mode 100644 index 000000000000..525b08e59cdf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/index.ts @@ -0,0 +1,2 @@ +export * from './umbraco-news.respository.js'; +export * from './umbraco-news.server.data-source.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/types.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/types.ts new file mode 100644 index 000000000000..fededb277fe4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/types.ts @@ -0,0 +1 @@ +export type * from './umbraco-news.server.data-source.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/umbraco-news.respository.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/umbraco-news.respository.ts new file mode 100644 index 000000000000..62c7ff9f4a06 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/umbraco-news.respository.ts @@ -0,0 +1,12 @@ +import { UmbNewsServerDataSource } from './umbraco-news.server.data-source.js'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import type { NewsDashboardResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export class UmbNewsDashboardRepository extends UmbRepositoryBase { + #dataSource = new UmbNewsServerDataSource(this); + + async getNewsDashboard(): Promise> { + return this.#dataSource.getNewsItems(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/umbraco-news.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/umbraco-news.server.data-source.ts new file mode 100644 index 000000000000..277447daa5e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/repository/umbraco-news.server.data-source.ts @@ -0,0 +1,25 @@ +import { tryExecute } from '@umbraco-cms/backoffice/resources'; +import { NewsDashboardService } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { NewsDashboardResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; + +export interface UmbNewsDataSource { + getNewsItems(): Promise>; +} + +/** + * A data source for the news items + * @class UmbNewsServerDataSource + * @implements {UmbNewsDataSource} + */ +export class UmbNewsServerDataSource extends UmbControllerBase implements UmbNewsDataSource { + /** + * Get all news items from the server + * @returns {*} + * @memberof UmbNewsServerDataSource + */ + async getNewsItems() { + return await tryExecute(this._host, NewsDashboardService.getNewsDashboard()); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/types.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/types.ts new file mode 100644 index 000000000000..e32ac4b889fe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/types.ts @@ -0,0 +1 @@ +export type * from './repository/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts index 7a1c506bb10b..ba1ef0e957a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts @@ -1,64 +1,198 @@ -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbNewsDashboardRepository } from './repository/index.js'; +import { + css, + customElement, + html, + nothing, + repeat, + state, + unsafeHTML, + when, +} from '@umbraco-cms/backoffice/external/lit'; +import { sanitizeHTML } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { NewsDashboardItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +interface UmbNewsDashboardGroupedItems { + priority: number; + items: Array; +} @customElement('umb-umbraco-news-dashboard') export class UmbUmbracoNewsDashboardElement extends UmbLitElement { - #infoLinks = [ - { - name: this.localize.term('welcomeDashboard_documentationHeadline'), - description: this.localize.term('welcomeDashboard_documentationDescription'), - href: 'https://docs.umbraco.com/?utm_source=core&utm_medium=dashboard&utm_campaign=docs', - }, - { - name: this.localize.term('welcomeDashboard_communityHeadline'), - description: this.localize.term('welcomeDashboard_communityDescription'), - href: 'https://our.umbraco.com/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=our_forum', - }, - { - name: this.localize.term('welcomeDashboard_resourcesHeadline'), - description: this.localize.term('welcomeDashboard_resourcesDescription'), - href: 'https://umbraco.com/resources/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=resources', - }, - { - name: this.localize.term('welcomeDashboard_trainingHeadline'), - description: this.localize.term('welcomeDashboard_trainingDescription'), - href: 'https://umbraco.com/training/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=training', - }, - ]; + @state() + private _items: Array = []; + + @state() + private _groupedItems: Array = []; + + @state() + private _loaded: boolean = false; + + #repo = new UmbNewsDashboardRepository(this); + + override async firstUpdated() { + const res = await this.#repo.getNewsDashboard(); + this._items = res.data?.items ?? []; + this._groupedItems = this.#groupItemsByPriority(); + this._loaded = true; + } + + #groupItemsByPriority(): Array { + const sanitizedItems = this._items.map((i) => ({ + ...i, + body: i.body ? sanitizeHTML(i.body) : '', + })); + + // Separate items by priority. + const priority1 = sanitizedItems.filter((item) => item.priority === 'High'); + const priority2 = sanitizedItems.filter((item) => item.priority === 'Medium'); + const priority3 = sanitizedItems.filter((item) => item.priority === 'Normal'); + + // Group 1: First 4 items from priority 1. + const group1Items = priority1.slice(0, 4); + const overflow1 = priority1.slice(4); + + // Group 2: Overflow from priority 1 + priority 2 items (max 4 total). + const group2Items = [...overflow1, ...priority2].slice(0, 4); + const overflow2Count = overflow1.length + priority2.length - 4; + const overflow2 = overflow2Count > 0 ? [...overflow1, ...priority2].slice(4) : []; + + // Group 3: Overflow from groups 1 & 2 + priority 3 items. + const group3Items = [...overflow2, ...priority3]; + + return [ + { priority: 1, items: group1Items }, + { priority: 2, items: group2Items }, + { priority: 3, items: group3Items }, + ]; + } override render() { + if (!this._loaded) { + return html`
`; + } + + if (this._items.length === 0) { + return this.#renderDefaultContent(); + } + return html` - diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts index 5fb10c3349a9..ac93c62bff2c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts @@ -190,7 +190,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper }, null, ); - // TODO: Implement index. + this.observe(this.#context.index, (index) => this.#updateBlockViewProps({ index }), null); this.observe( this.#context.label, (label) => { @@ -517,6 +517,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper class="umb-block-grid__block--view" .label=${this._label} .icon=${this._icon} + .index=${this._blockViewProps.index} .unpublished=${!this._exposed} .config=${this._blockViewProps.config} .content=${this._blockViewProps.content} @@ -529,6 +530,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper class="umb-block-grid__block--view" .label=${this._label} .icon=${this._icon} + .index=${this._blockViewProps.index} .unpublished=${!this._exposed} .config=${this._blockViewProps.config} .content=${this._blockViewProps.content} diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts index 5e3a68e444a7..554dfd36b546 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts @@ -29,19 +29,15 @@ import '../unsupported-list-block/index.js'; */ @customElement('umb-block-list-entry') export class UmbBlockListEntryElement extends UmbLitElement implements UmbPropertyEditorUiElement { - // @property({ type: Number }) - public get index(): number | undefined { - return this.#context.getIndex(); - } public set index(value: number | undefined) { this.#context.setIndex(value); } + public get index(): number | undefined { + return this.#context.getIndex(); + } @property({ attribute: false }) - public get contentKey(): string | undefined { - return this._contentKey; - } public set contentKey(value: string | undefined) { if (!value) return; this._contentKey = value; @@ -57,6 +53,9 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper 'observeMessagesForContent', ); } + public get contentKey(): string | undefined { + return this._contentKey; + } private _contentKey?: string | undefined; #context = new UmbBlockListEntryContext(this); @@ -147,7 +146,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper }, null, ); - // TODO: Implement index. + this.observe(this.#context.index, (index) => this.#updateBlockViewProps({ index }), null); this.observe( this.#context.label, (label) => { @@ -374,6 +373,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/ref-list-block/ref-list-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/ref-list-block/ref-list-block.element.ts index 1be1ace010b8..574f72143c69 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/ref-list-block/ref-list-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/ref-list-block/ref-list-block.element.ts @@ -14,6 +14,9 @@ export class UmbRefListBlockElement extends UmbLitElement { @property({ type: String, reflect: false }) icon?: string; + @property({ type: Number, attribute: false }) + index?: number; + @property({ type: Boolean, reflect: true }) unpublished?: boolean; @@ -27,7 +30,7 @@ export class UmbRefListBlockElement extends UmbLitElement { config?: UmbBlockEditorCustomViewConfiguration; override render() { - const blockValue = { ...this.content, $settings: this.settings }; + const blockValue = { ...this.content, $settings: this.settings, $index: this.index }; return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index 943d79c51dd6..ddc7fe59671c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -373,12 +373,13 @@ export class UmbPropertyEditorUIBlockListElement return html` ${repeat( this._layouts, - (x) => x.contentKey, - (layoutEntry, index) => html` + (layout, index) => `${index}_${layout.contentKey}`, + (layout, index) => html` ${this.#renderInlineCreateButton(index)} `, @@ -391,7 +392,7 @@ export class UmbPropertyEditorUIBlockListElement if (this.readonly && this._layouts.length > 0) { return nothing; } else { - return html` ${this.#renderCreateButton()} ${this.#renderPasteButton()} `; + return html`${this.#renderCreateButton()}${this.#renderPasteButton()}`; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts index 63d79ac8f888..e9624ed2ebfd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts @@ -22,9 +22,6 @@ import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/ext @customElement('umb-rte-block') export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropertyEditorUiElement { @property({ type: String, attribute: 'data-content-key', reflect: true }) - public get contentKey(): string | undefined { - return this._contentKey; - } public set contentKey(value: string | undefined) { if (!value) return; this._contentKey = value; @@ -40,6 +37,9 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert 'observeMessagesForContent', ); } + public get contentKey(): string | undefined { + return this._contentKey; + } private _contentKey?: string | undefined; #context = new UmbBlockRteEntryContext(this); @@ -138,7 +138,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert }, null, ); - // TODO: Implement index. + this.observe(this.#context.index, (index) => this.#updateBlockViewProps({ index }), null); this.observe( this.#context.label, (label) => { @@ -292,7 +292,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert #renderActionBar() { return this._showActions - ? html` ${this.#renderEditAction()} ${this.#renderEditSettingsAction()} ` + ? html`${this.#renderEditAction()}${this.#renderEditSettingsAction()}` : nothing; } @@ -308,6 +308,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert return html` From 494674d35422f96099ad6b30b14f5e0a9f3222f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=BCger?= <93977820+OskarKruger@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:20:01 +0200 Subject: [PATCH 024/155] Entity Actions: More create button discernible text, extension of #20434 (#20458) * added hovering and focus border to RTE * fix main to OG * fix to main again * I'm going to cry * Missing localiztion feature, maybe UmbLitElement? * added localization controller to fetch localized version * localization successful for viewActionsFor and CreateFor * clean up button text * Changed label for content header to display proper name * clean up code * Included button labels for media section * clean code * Relocated localization keys, as `actions_viewActionsFor` already existed. Also made into a function, to support a fallback label. * Simplified the "Create for" label/localization Removed the need for a `getCreateAriaLabel()` method. * Removed the double-localizations (of `actions_viewActionsFor`) as the "umb-entity-actions-bundle" component handles this now. * imports tidy-up * Simplified localization key condition * switched to new localization key for other sections for new labeling * Bumped `@umbraco/playwright-testhelpers` 16.0.55 https://github.com/umbraco/Umbraco.Playwright.Testhelpers/releases/tag/release%2F16.0.55 --------- Co-authored-by: Oskar kruger Co-authored-by: leekelleher --- .../packages/core/collection/action/create/manifests.ts | 2 +- .../src/packages/dictionary/entity-action/manifests.ts | 2 +- .../entity-actions/create/manifests.ts | 2 +- .../document-types/entity-actions/create/manifests.ts | 2 +- .../documents/user-permissions/document/manifests.ts | 2 +- .../src/packages/language/entity-actions/manifests.ts | 2 +- .../members/member-group/entity-actions/manifests.ts | 2 +- .../members/member/entity-actions/create/manifests.ts | 2 +- .../partial-views/entity-actions/create/manifests.ts | 2 +- .../templating/scripts/entity-actions/create/manifests.ts | 2 +- .../templating/templates/entity-actions/manifests.ts | 2 +- tests/Umbraco.Tests.AcceptanceTest/package-lock.json | 8 ++++---- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/action/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/action/create/manifests.ts index 429fb41b26b9..c12d8a346313 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/action/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/action/create/manifests.ts @@ -12,7 +12,7 @@ export const manifests: Array = element: () => import('./collection-create-action.element.js'), weight: 1200, meta: { - label: '#actions_create', + label: '#actions_createFor', }, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/manifests.ts index 66c2670c34f8..b267083ba959 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/manifests.ts @@ -13,7 +13,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_DICTIONARY_ENTITY_TYPE, UMB_DICTIONARY_ROOT_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#general_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/manifests.ts index b945f804e0ab..03f28adc56b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/manifests.ts @@ -11,7 +11,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_DOCUMENT_BLUEPRINT_ROOT_ENTITY_TYPE, UMB_DOCUMENT_BLUEPRINT_FOLDER_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts index f0a18c7cb745..d80976449b20 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts @@ -17,7 +17,7 @@ export const manifests: Array = ], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, headline: '#create_createUnder #treeHeaders_documentTypes', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/manifests.ts index 03bebae42416..f8aec35d4bfd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/manifests.ts @@ -65,7 +65,7 @@ const permissions: Array = [ forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { verbs: ['Umb.Document.Create'], - label: '#actions_create', + label: '#actions_createFor', description: '#actionDescriptions_create', }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/entity-actions/manifests.ts index f917dc1d27f2..0e7d7d346627 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/entity-actions/manifests.ts @@ -23,7 +23,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_LANGUAGE_ROOT_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/entity-actions/manifests.ts index e450465c720a..e00131cd07df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/entity-actions/manifests.ts @@ -12,7 +12,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_MEMBER_GROUP_ROOT_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/entity-actions/create/manifests.ts index 9216d08ffc3a..a382103aa3da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/entity-actions/create/manifests.ts @@ -11,7 +11,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_MEMBER_ROOT_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/manifests.ts index 6ba2c40dc8c8..242d9100a719 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/manifests.ts @@ -11,7 +11,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_PARTIAL_VIEW_ROOT_ENTITY_TYPE, UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/manifests.ts index 68cb772ad2b0..3c94201b59ed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/manifests.ts @@ -11,7 +11,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_SCRIPT_ROOT_ENTITY_TYPE, UMB_SCRIPT_FOLDER_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/entity-actions/manifests.ts index 1ee92fff8159..4485d593ffeb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/entity-actions/manifests.ts @@ -13,7 +13,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_TEMPLATE_ENTITY_TYPE, UMB_TEMPLATE_ROOT_ENTITY_TYPE], meta: { icon: 'icon-add', - label: '#actions_create', + label: '#actions_createFor', additionalOptions: true, }, }, diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index b1e7b97b7a08..6a65872266d9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.40", - "@umbraco/playwright-testhelpers": "^16.0.50", + "@umbraco/playwright-testhelpers": "^16.0.55", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "16.0.50", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.50.tgz", - "integrity": "sha512-2tHQUdv8lCV4O47hGMvyC+JJfG4//fSSE/gUOVfFNDEHzhU5mSZ8f9oGKob8XOv2RB7ynZFYtIIxEMZIWrGFDA==", + "version": "16.0.55", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.55.tgz", + "integrity": "sha512-715l112FHB7snWq0sY7e0fUD2ppWSSysBKHFhcQkGGw+3Gbo68Z6iXfeAketzKohWji19un4KC3mvZU0IICr9g==", "license": "MIT", "dependencies": { "@umbraco/json-models-builders": "2.0.40", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 6deebfb3fdcb..adf43dd3260b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.40", - "@umbraco/playwright-testhelpers": "^16.0.50", + "@umbraco/playwright-testhelpers": "^16.0.55", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" From b5662d9c8901e621bc95cdd169b2ee2801958797 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:39:53 +0200 Subject: [PATCH 025/155] Dependencies: Remove `Microsoft.CodeAnalysis.CSharp` dependency from Umbraco.Infrastructure (#20481) * Remove Microsoft.CodeAnalysis.CSharp from Infrastructure project This was only needed for runtime compilation and thus is no longer needed in Infrastructure. It also caused dependency problems with EF Core Design in previous versions. * Disable CPM for UI project to better reflect consumers This will ensure that we face any potential dependency issues consumers are also likely to run into. * Add `Microsoft.CodeAnalysis.CSharp` reference to `Umbraco.Cms.DevelopmentMode.Backoffice` --- Directory.Packages.props | 5 ----- .../Umbraco.Cms.DevelopmentMode.Backoffice.csproj | 3 ++- .../Umbraco.Infrastructure.csproj | 1 - src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 15 +++------------ 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e2aa9d1c3fbe..60d2e5d102d4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,14 +14,9 @@ - - - - - diff --git a/src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj b/src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj index 635bbeed4cb9..bacbbd7cbf9a 100644 --- a/src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj +++ b/src/Umbraco.Cms.DevelopmentMode.Backoffice/Umbraco.Cms.DevelopmentMode.Backoffice.csproj @@ -8,10 +8,11 @@ + - + diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 1004517b4400..06e73265fcaa 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -40,7 +40,6 @@ - diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 37fd91a3e544..21bff12dca08 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -5,6 +5,7 @@ false false + false
@@ -24,23 +25,13 @@ - - - - - - - - - - - + - + From 12adfd52bdb9374bb139670518bb83f9bd5ca791 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 14 Oct 2025 11:22:21 +0200 Subject: [PATCH 026/155] Performance: Reduce number of database calls in save and publish operations (#20485) * Added request caching to media picker media retrieval, to improve performance in save operations. * WIP: Update or insert in bulk when updating property data. * Add tests verifying UpdateBatch. * Fixed issue with UpdateBatch and SQL Server. * Removed stopwatch. * Fix test on SQLite (failing on SQLServer). * Added temporary test for direct call to NPoco UpdateBatch. * Fixed test on SQLServer. * Add integration test verifying the same property data is persisted as before the performance refactor. * Log expected warning in DocumentUrlService as debug. --- .../Services/DocumentUrlService.cs | 3 +- .../Implement/ContentRepositoryBase.cs | 21 ++++-- .../MediaPicker3PropertyEditor.cs | 27 ++++++- .../ContentPublishingServiceTests.Publish.cs | 28 ++++++++ .../Persistence/NPocoTests/NPocoFetchTests.cs | 3 - .../NPocoTests/NPocoUpdateBatchTests.cs | 70 +++++++++++++++++++ 6 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoUpdateBatchTests.cs diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index 5d27f6d634bb..12f882365202 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -432,7 +432,8 @@ private void HandleCaching(IScopeContext scopeContext, IContent document, string if (draftUrlSegments.Any() is false) { - _logger.LogWarning("No draft URL segments found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}"); + // Log at debug level because this is expected when a document is not published in a given language. + _logger.LogDebug("No draft URL segments found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}"); } else { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 82fc8f911993..4426374af246 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -1132,25 +1132,38 @@ protected void ReplacePropertyValues(TEntity entity, int versionId, int publishe IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); + var toUpdate = new List(); + var toInsert = new List(); foreach (PropertyDataDto propertyDataDto in propertyDataDtos) { // Check if this already exists and update, else insert a new one if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out PropertyDataDto? propData)) { propertyDataDto.Id = propData.Id; - Database.Update(propertyDataDto); + toUpdate.Add(propertyDataDto); } else { - // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs. - // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases. - Database.Insert(propertyDataDto); + toInsert.Add(propertyDataDto); } // track which ones have been processed existingPropDataIds.Remove(propertyDataDto.Id); } + if (toUpdate.Count > 0) + { + var updateBatch = toUpdate + .Select(x => UpdateBatch.For(x)) + .ToList(); + Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 }); + } + + if (toInsert.Count > 0) + { + Database.InsertBulk(toInsert); + } + // For any remaining that haven't been processed they need to be deleted if (existingPropDataIds.Count > 0) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index 26efa02f1029..d4a9e492bd17 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -54,6 +54,8 @@ protected override IDataValueEditor CreateValueEditor() => /// internal sealed class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference { + private const string MediaCacheKeyFormat = nameof(MediaPicker3PropertyValueEditor) + "_Media_{0}"; + private readonly IDataTypeConfigurationCache _dataTypeReadCache; private readonly IJsonSerializer _jsonSerializer; private readonly IMediaImportService _mediaImportService; @@ -61,6 +63,7 @@ internal sealed class MediaPicker3PropertyValueEditor : DataValueEditor, IDataVa private readonly ITemporaryFileService _temporaryFileService; private readonly IScopeProvider _scopeProvider; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly AppCaches _appCaches; /// /// Initializes a new instance of the class. @@ -93,6 +96,8 @@ public MediaPicker3PropertyValueEditor( _scopeProvider = scopeProvider; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _dataTypeReadCache = dataTypeReadCache; + _appCaches = appCaches; + var validators = new TypedJsonValidatorRunner, MediaPicker3Configuration>( jsonSerializer, new MinMaxValidator(localizedTextService), @@ -203,13 +208,31 @@ private List UpdateMediaTypeAliases(List m foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos) { - IMedia? media = _mediaService.GetById(mediaWithCropsDto.MediaKey); + IMedia? media = GetMediaById(mediaWithCropsDto.MediaKey); mediaWithCropsDto.MediaTypeAlias = media?.ContentType.Alias ?? unknownMediaType; } return mediaWithCropsDtos.Where(m => m.MediaTypeAlias != unknownMediaType).ToList(); } + private IMedia? GetMediaById(Guid key) + { + // Cache media lookups in case the same media is handled multiple times across a save operation, + // which is possible, particularly if we have multiple languages and blocks. + var cacheKey = string.Format(MediaCacheKeyFormat, key); + IMedia? media = _appCaches.RequestCache.GetCacheItem(cacheKey); + if (media is null) + { + media = _mediaService.GetById(key); + if (media is not null) + { + _appCaches.RequestCache.Set(cacheKey, media); + } + } + + return media; + } + private List HandleTemporaryMediaUploads(List mediaWithCropsDtos, MediaPicker3Configuration configuration) { var invalidDtos = new List(); @@ -217,7 +240,7 @@ private List HandleTemporaryMediaUploads(List(); + Assert.AreEqual(18, dtos.Count); // 3 properties * 3 cultures * 2 (published + edited). + scope.Complete(); + } + + var publishAttempt = await ContentPublishingService.PublishAsync( + content.Key, + [new() { Culture = langEn.IsoCode }], + Constants.Security.SuperUserKey); + Assert.IsTrue(publishAttempt.Success); + + using (var scope = ScopeProvider.CreateScope()) + { + var dtos = scope.Database.Fetch(); + Assert.AreEqual(19, dtos.Count); // + 3 for published populated title property, - 2 for existing published properties of other cultures. + scope.Complete(); + } + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs index b7996b779f7c..d7885bd27f61 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs @@ -1,13 +1,10 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using NPoco; using NUnit.Framework; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; -using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPocoTests; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoUpdateBatchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoUpdateBatchTests.cs new file mode 100644 index 000000000000..ff35ea259c90 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoUpdateBatchTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using NPoco; +using NUnit.Framework; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPocoTests; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +internal sealed class NPocoUpdateBatchTests : UmbracoIntegrationTest +{ + [Test] + public void Can_Update_Batch() + { + // Arrange + var servers = new List(); + for (var i = 0; i < 3; i++) + { + servers.Add(new ServerRegistrationDto + { + Id = i + 1, + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = false, + DateAccessed = DateTime.Now, + }); + } + + using (var scope = ScopeProvider.CreateScope()) + { + scope.Database.BulkInsertRecords(servers); + scope.Complete(); + } + + // Act + for (var i = 0; i < 3; i++) + { + servers[i].ServerAddress = "newaddress" + i; + servers[i].IsActive = true; + } + + using (var scope = ScopeProvider.CreateScope()) + { + var updateBatch = servers + .Select(x => UpdateBatch.For(x)) + .ToList(); + var updated = scope.Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 }); + Assert.AreEqual(3, updated); + scope.Complete(); + } + + // Assert + using (var scope = ScopeProvider.CreateScope()) + { + var dtos = scope.Database.Fetch(); + Assert.AreEqual(3, dtos.Count); + for (var i = 0; i < 3; i++) + { + Assert.AreEqual(servers[i].ServerAddress, dtos[i].ServerAddress); + Assert.AreEqual(servers[i].ServerIdentity, dtos[i].ServerIdentity); + Assert.AreEqual(servers[i].IsActive, dtos[i].IsActive); + } + } + } +} From e53220c8f5a9a8c28bd433b974a3cd6cadc584a3 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Tue, 14 Oct 2025 01:15:01 -0900 Subject: [PATCH 027/155] Delivery API: Fix not reindexing branch descendants when branch root already published but unchanged (closes #20370) (#20462) * Fix deliveryApi not reindexing branch descendants when branch root already published and unchanged * Commit update and name improvement --- ...veryApiContentIndexHandleContentChanges.cs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentChanges.cs b/src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentChanges.cs index fb0d2c92193f..73078b58686a 100644 --- a/src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentChanges.cs +++ b/src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentChanges.cs @@ -59,7 +59,19 @@ public void Execute() => _backgroundTaskQueue.QueueBackgroundWorkItem(_ => RemoveFromIndex(pendingRemovals, index); pendingRemovals.Clear(); - Reindex(content, index); + ReIndexResult reIndexResult = Reindex(content, index); + + + // When we get to this point, we are dealing with either + // a refresh node or a refresh branch (see reindex =...). + // A refresh branch can be many things, the Reindex function takes care of most scenarios. + // But it only reindexes descendants if the base node has any changed cultures (see comments in that function) + // So by checking what kind of operation it did when the initial indexrequest is for a refresh branch, + // we can support reindexing a branch while the base node was unchanged. + if (reIndexResult == ReIndexResult.Updated && changeTypes.HasType(TreeChangeTypes.RefreshBranch)) + { + ReindexDescendants(content, index); + } } } @@ -68,7 +80,7 @@ public void Execute() => _backgroundTaskQueue.QueueBackgroundWorkItem(_ => return Task.CompletedTask; }); - private void Reindex(IContent content, IIndex index) + private ReIndexResult Reindex(IContent content, IIndex index) { // get the currently indexed cultures for the content CulturePublishStatus[] existingCultures = index @@ -95,16 +107,19 @@ private void Reindex(IContent content, IIndex index) // we likely got here because a removal triggered a "refresh branch" notification, now we // need to delete every last culture of this content and all descendants RemoveFromIndex(content.Id, index); - return; + return ReIndexResult.Removed; } - // if the published state changed of any culture, chances are there are similar changes ot the content descendants + // if the published state changed of any culture, chances are there are similar changes at the content descendants // that need to be reflected in the index, so we'll reindex all descendants var changedCulturePublishStatus = indexedCultures.Intersect(existingCultures).Count() != existingCultures.Length; if (changedCulturePublishStatus) { ReindexDescendants(content, index); + return ReIndexResult.UpdatedWithDescendants; } + + return ReIndexResult.Updated; } private CulturePublishStatus[] UpdateIndex(IContent content, IIndex index) @@ -179,4 +194,11 @@ public override bool Equals(object? obj) public override int GetHashCode() => HashCode.Combine(Culture, Published); } + + private enum ReIndexResult + { + Updated, + UpdatedWithDescendants, + Removed, + } } From ac56bffef2bb7c42f4acc8e96288c88a879309d9 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 14 Oct 2025 12:26:25 +0200 Subject: [PATCH 028/155] fix import in storybook of moved file --- src/Umbraco.Web.UI.Client/.storybook/preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/.storybook/preview.js b/src/Umbraco.Web.UI.Client/.storybook/preview.js index 98c8ab65b5e8..1ea461cf076e 100644 --- a/src/Umbraco.Web.UI.Client/.storybook/preview.js +++ b/src/Umbraco.Web.UI.Client/.storybook/preview.js @@ -47,7 +47,7 @@ import { manifests as propertyEditorManifests } from '../src/packages/property-e import { manifests as publishCacheManifests } from '../src/packages/publish-cache/manifests'; import { manifests as relationsManifests } from '../src/packages/relations/manifests'; import { manifests as rteManifests } from '../src/packages/rte/manifests'; -import { manifests as searchManifests } from '../src/packages/search/manifests'; +import { manifests as searchManifests } from '../src/packages/core/search/manifests'; import { manifests as segmentManifests } from '../src/packages/segment/manifests'; import { manifests as settingsManifests } from '../src/packages/settings/manifests'; import { manifests as staticFileManifests } from '../src/packages/static-file/manifests'; From cdf9ee4566aea65dba1d7544137e0b3364958ae6 Mon Sep 17 00:00:00 2001 From: Anders Reus <88318565+andersreus@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:06:48 +0200 Subject: [PATCH 029/155] Added culture to the ApiContentRouteBuilder to include variant languages. (#20366) (#20499) Added culture to the ApiContentRouteBuilder to include variant languages. --- src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs b/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs index 209ce4e33e2e..2ae47082108c 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs @@ -45,7 +45,7 @@ protected ApiContentBuilderBase( public virtual T? Build(IPublishedContent content) { - IApiContentRoute? route = ApiContentRouteBuilder.Build(content); + IApiContentRoute? route = ApiContentRouteBuilder.Build(content, VariationContextAccessor.VariationContext?.Culture); if (route is null) { return default; From a19b9fb5fe162d21776cdfd269cbed8e41cc1a25 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:35:42 +0000 Subject: [PATCH 030/155] UFM: Add camelCase aliases for UFM filters to support UFMJS expressions (closes #20500) (#20501) * Initial plan * Add camelCase aliases for UFM filters with hyphens (stripHtml, titleCase, wordLimit) Co-authored-by: iOvergaard <752371+iOvergaard@users.noreply.github.com> * Add manifest tests for camelCase filter aliases Co-authored-by: iOvergaard <752371+iOvergaard@users.noreply.github.com> * discards tests that are not useful * test: updates imports for stripHtml api * Exports `UmbUfmStripHtmlFilterApi` class --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: iOvergaard <752371+iOvergaard@users.noreply.github.com> Co-authored-by: leekelleher --- .../src/packages/ufm/filters/manifests.ts | 24 ++++++++++ .../ufm/filters/strip-html.filter.test.ts | 47 +++++++++++++++++++ .../packages/ufm/filters/strip-html.filter.ts | 1 + 3 files changed, 72 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/ufm/filters/strip-html.filter.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/manifests.ts index 265b99358ced..8efad58bd0c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/manifests.ts @@ -29,6 +29,14 @@ export const manifests: Array = [ api: () => import('./strip-html.filter.js'), meta: { alias: 'strip-html' }, }, + // TODO: Remove in V18 - replaced by camelCase alias below for UFMJS compatibility + { + type: 'ufmFilter', + alias: 'Umb.Filter.StripHtmlCamelCase', + name: 'Strip HTML UFM Filter (camelCase)', + api: () => import('./strip-html.filter.js'), + meta: { alias: 'stripHtml' }, + }, { type: 'ufmFilter', alias: 'Umb.Filter.TitleCase', @@ -36,6 +44,14 @@ export const manifests: Array = [ api: () => import('./title-case.filter.js'), meta: { alias: 'title-case' }, }, + // TODO: Remove in V18 - replaced by camelCase alias below for UFMJS compatibility + { + type: 'ufmFilter', + alias: 'Umb.Filter.TitleCaseCamelCase', + name: 'Title Case UFM Filter (camelCase)', + api: () => import('./title-case.filter.js'), + meta: { alias: 'titleCase' }, + }, { type: 'ufmFilter', alias: 'Umb.Filter.Truncate', @@ -57,4 +73,12 @@ export const manifests: Array = [ api: () => import('./word-limit.filter.js'), meta: { alias: 'word-limit' }, }, + // TODO: Remove in V18 - replaced by camelCase alias below for UFMJS compatibility + { + type: 'ufmFilter', + alias: 'Umb.Filter.WordLimitCamelCase', + name: 'Word Limit UFM Filter (camelCase)', + api: () => import('./word-limit.filter.js'), + meta: { alias: 'wordLimit' }, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/strip-html.filter.test.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/strip-html.filter.test.ts new file mode 100644 index 000000000000..45c39844b6f4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/strip-html.filter.test.ts @@ -0,0 +1,47 @@ +import { expect } from '@open-wc/testing'; +import { UmbUfmStripHtmlFilterApi } from './strip-html.filter.js'; + +describe('UmbUfmStripHtmlFilter', () => { + let filter: UmbUfmStripHtmlFilterApi; + + beforeEach(() => { + filter = new UmbUfmStripHtmlFilterApi(); + }); + + describe('filter', () => { + it('should strip HTML tags from string', () => { + const result = filter.filter('

Hello World

'); + expect(result).to.equal('Hello World'); + }); + + it('should handle empty string', () => { + const result = filter.filter(''); + expect(result).to.equal(''); + }); + + it('should handle null input', () => { + const result = filter.filter(null); + expect(result).to.equal(''); + }); + + it('should handle undefined input', () => { + const result = filter.filter(undefined); + expect(result).to.equal(''); + }); + + it('should handle markup object', () => { + const result = filter.filter({ markup: '

Test

' }); + expect(result).to.equal('Test'); + }); + + it('should strip complex HTML', () => { + const result = filter.filter('

Title

Paragraph with link

'); + expect(result).to.equal('TitleParagraph with link'); + }); + + it('should handle plain text without HTML', () => { + const result = filter.filter('Plain text'); + expect(result).to.equal('Plain text'); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/strip-html.filter.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/strip-html.filter.ts index 98b7e394d836..a65969e52243 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/strip-html.filter.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/filters/strip-html.filter.ts @@ -13,3 +13,4 @@ class UmbUfmStripHtmlFilterApi extends UmbUfmFilterBase { } export { UmbUfmStripHtmlFilterApi as api }; +export { UmbUfmStripHtmlFilterApi }; From e22b459d9c72ac7f738fb7ea163713cee86aa07d Mon Sep 17 00:00:00 2001 From: NguyenThuyLan <116753400+NguyenThuyLan@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:33:24 +0700 Subject: [PATCH 031/155] WorkspaceView: Add tests for create and using custom workspace view (#20408) * WorkspaceView: Add tests for create and using custom workspace view * update helper version * Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> * Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> * update format code * Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> * Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> * Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> --------- Co-authored-by: Lan Nguyen Thuy Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> --- .../workspace-view/umbraco-package.json | 24 ++++++++++ .../workspace-view/workspace-view.js | 28 ++++++++++++ .../workspace-view/workspace-view.js.map | 1 + .../ExtensionRegistry/WorkspaceView.spec.ts | 44 +++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/umbraco-package.json create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/workspace-view.js create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/workspace-view.js.map create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/umbraco-package.json b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/umbraco-package.json new file mode 100644 index 000000000000..42d4c1957f7a --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/umbraco-package.json @@ -0,0 +1,24 @@ +{ + "$schema": "../../umbraco-package-schema.json", + "name": "My workspace", + "version": "0.1.0", + "extensions": [ + { + "type": "workspaceView", + "alias": "My.WorkspaceView", + "name": "My Workspace View", + "element": "/App_Plugins/workspace-view/workspace-view.js", + "meta": { + "label": "My Workspace View", + "pathname": "/my-workspace-view", + "icon": "icon-add" + }, + "conditions": [ + { + "alias": "Umb.Condition.WorkspaceAlias", + "match": "Umb.Workspace.Document" + } + ] + } + ] +} diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/workspace-view.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/workspace-view.js new file mode 100644 index 000000000000..b471d1e7cf1a --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/workspace-view.js @@ -0,0 +1,28 @@ +import { LitElement as n, html as a, css as c, customElement as p } from "@umbraco-cms/backoffice/external/lit"; +import { UmbElementMixin as u } from "@umbraco-cms/backoffice/element-api"; +var w = Object.getOwnPropertyDescriptor, v = (o, s, i, l) => { + for (var e = l > 1 ? void 0 : l ? w(s, i) : s, r = o.length - 1, m; r >= 0; r--) + (m = o[r]) && (e = m(e) || e); + return e; +}; +let t = class extends u(n) { + render() { + return a` + + Welcome to my newly created workspace view. + + `; + } +}; +t.styles = c` + uui-box { + margin: 20px; + } + `; +t = v([ + p("my-workspaceview") +], t); +export { + t as default +}; +//# sourceMappingURL=workspace-view.js.map diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/workspace-view.js.map b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/workspace-view.js.map new file mode 100644 index 000000000000..42be166b3e3f --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/workspace-view/workspace-view.js.map @@ -0,0 +1 @@ +{"version":3,"file":"workspace-view.js","sources":["../../workspace-view/src/my-element.ts"],"sourcesContent":["import { LitElement, html, customElement, css } from \"@umbraco-cms/backoffice/external/lit\";\nimport { UmbElementMixin } from \"@umbraco-cms/backoffice/element-api\";\n\n@customElement('my-workspaceview')\nexport default class MyWorkspaceViewElement extends UmbElementMixin(LitElement) {\n\n render() {\n return html` \n \n Welcome to my newly created workspace view.\n \n `\n }\n\n static styles = css`\n uui-box {\n margin: 20px;\n }\n `\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'my-workspaceview': MyWorkspaceViewElement\n }\n}\n"],"names":["MyWorkspaceViewElement","UmbElementMixin","LitElement","html","css","__decorateClass","customElement"],"mappings":";;;;;;;AAIA,IAAqBA,IAArB,cAAoDC,EAAgBC,CAAU,EAAE;AAAA,EAE5E,SAAS;AACL,WAAOC;AAAA;AAAA;AAAA;AAAA;AAAA,EAKX;AAOJ;AAfqBH,EAUV,SAASI;AAAA;AAAA;AAAA;AAAA;AAVCJ,IAArBK,EAAA;AAAA,EADCC,EAAc,kBAAkB;AAAA,GACZN,CAAA;"} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts new file mode 100644 index 000000000000..b5d166fe86a6 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/WorkspaceView.spec.ts @@ -0,0 +1,44 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +// Content +const contentName = 'TestContent'; +// DocumentType +const documentTypeName = 'TestDocumentTypeForContent'; +// DataType +const dataTypeName = 'Textstring'; +// Media +const mediaName = 'TestMedia'; + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can see the custom workspace view in the content section', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, 'Test content', dataTypeName); + + // Act + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Assert + await umbracoUi.content.isWorkspaceViewTabWithAliasVisible('My.WorkspaceView', true); +}); + +test('cannot see the custom workspace view in the media section', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.media.createDefaultMediaWithImage(mediaName); + + // Act + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.media); + await umbracoUi.media.goToMediaWithName(mediaName); + + // Assert + await umbracoUi.media.isWorkspaceViewTabWithAliasVisible('My.WorkspaceView', false); +}); \ No newline at end of file From 1ab13a970b9d7a8d648c786858c5692e96690178 Mon Sep 17 00:00:00 2001 From: NguyenThuyLan <116753400+NguyenThuyLan@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:33:41 +0700 Subject: [PATCH 032/155] Dashboard: Add tests for create and using custom dashboard (#20253) * add tests for custom dashboard * update test dashboard using helper * remove extensionRegistry for playwright config * update helper version for dashboard * Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomDashboard.spec.ts Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> * fix format code --------- Co-authored-by: Lan Nguyen Thuy Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> --- .../welcome-dashboard/umbraco-package.json | 25 ++++++++++++ .../welcome-dashboard/welcome-dashboard.js | 38 +++++++++++++++++++ .../welcome-dashboard.js.map | 1 + .../ExtensionRegistry/CustomDashboard.spec.ts | 22 +++++++++++ 4 files changed, 86 insertions(+) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/umbraco-package.json create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/welcome-dashboard.js create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/welcome-dashboard.js.map create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomDashboard.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/umbraco-package.json b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/umbraco-package.json new file mode 100644 index 000000000000..d8e628762cd2 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/umbraco-package.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../umbraco-package-schema.json", + "name": "My.WelcomePackage", + "version": "0.1.0", + "extensions": [ + { + "type": "dashboard", + "alias": "my.welcome.dashboard", + "name": "My Welcome Dashboard", + "element": "/App_Plugins/welcome-dashboard/welcome-dashboard.js", + "elementName": "my-welcome-dashboard", + "weight": 30, + "meta": { + "label": "Welcome Dashboard", + "pathname": "welcome-dashboard" + }, + "conditions": [ + { + "alias": "Umb.Condition.SectionAlias", + "match": "Umb.Section.Content" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/welcome-dashboard.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/welcome-dashboard.js new file mode 100644 index 000000000000..8ddf0fbaa3a0 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/welcome-dashboard.js @@ -0,0 +1,38 @@ +import { css as n, customElement as c, html as d } from "@umbraco-cms/backoffice/external/lit"; +import { UmbLitElement as p } from "@umbraco-cms/backoffice/lit-element"; +var i = Object.getOwnPropertyDescriptor, h = (r, s, l, a) => { + for (var e = a > 1 ? void 0 : a ? i(s, l) : s, o = r.length - 1, m; o >= 0; o--) + (m = r[o]) && (e = m(e) || e); + return e; +}; +let t = class extends p { + render() { + return d` +

Welcome Dashboard

+
+

+ This is the Backoffice. From here, you can modify the content, + media, and settings of your website. +

+

© Sample Company 20XX

+
+ `; + } +}; +t.styles = [ + n` + :host { + display: block; + padding: 24px; + } + ` +]; +t = h([ + c("my-welcome-dashboard") +], t); +const b = t; +export { + t as MyWelcomeDashboardElement, + b as default +}; +//# sourceMappingURL=welcome-dashboard.js.map diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/welcome-dashboard.js.map b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/welcome-dashboard.js.map new file mode 100644 index 000000000000..65539b9a6582 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/welcome-dashboard/welcome-dashboard.js.map @@ -0,0 +1 @@ +{"version":3,"file":"welcome-dashboard.js","sources":["../../welcome-dashboard/src/welcome-dashboard.element.ts"],"sourcesContent":["import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';\r\nimport { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';\r\n\r\n@customElement('my-welcome-dashboard')\r\nexport class MyWelcomeDashboardElement extends UmbLitElement {\r\n\r\n override render() {\r\n return html`\r\n

Welcome Dashboard

\r\n
\r\n

\r\n This is the Backoffice. From here, you can modify the content,\r\n media, and settings of your website.\r\n

\r\n

© Sample Company 20XX

\r\n
\r\n `;\r\n }\r\n\r\n static override readonly styles = [\r\n css`\r\n :host {\r\n display: block;\r\n padding: 24px;\r\n }\r\n `,\r\n ];\r\n}\r\n\r\nexport default MyWelcomeDashboardElement;\r\n\r\ndeclare global {\r\n interface HTMLElementTagNameMap {\r\n 'my-welcome-dashboard': MyWelcomeDashboardElement;\r\n }\r\n}"],"names":["MyWelcomeDashboardElement","UmbLitElement","html","css","__decorateClass","customElement","MyWelcomeDashboardElement$1"],"mappings":";;;;;;;AAIO,IAAMA,IAAN,cAAwCC,EAAc;AAAA,EAEhD,SAAS;AACd,WAAOC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUX;AAUJ;AAvBaF,EAegB,SAAS;AAAA,EAC9BG;AAAA;AAAA;AAAA;AAAA;AAAA;AAMJ;AAtBSH,IAANI,EAAA;AAAA,EADNC,EAAc,sBAAsB;AAAA,GACxBL,CAAA;AAyBb,MAAAM,IAAeN;"} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomDashboard.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomDashboard.spec.ts new file mode 100644 index 000000000000..0303f6b9af38 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomDashboard.spec.ts @@ -0,0 +1,22 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +// Dashboard +const dashboardName = 'Welcome Dashboard'; + +test('can see the custom dashboard in content section', async ({umbracoUi}) => { + // Act + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Assert + await umbracoUi.content.isDashboardTabWithNameVisible(dashboardName, true); +}); + +test('can not see the custom dashboard in media section', async ({umbracoUi}) => { + // Act + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.media); + + // Assert + await umbracoUi.content.isDashboardTabWithNameVisible(dashboardName, false); +}); \ No newline at end of file From fdf759d08debcbf4d86fed2d6d2c6aaed16ef03e Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 15 Oct 2025 09:41:41 +0200 Subject: [PATCH 033/155] Content Types: Prevent creation of document type with an alias that case insensitively matches an existing alias (closes #20467) (#20471) Prevent creation of document type with an alias that case insensitively matches an existing alias. --- .../ContentTypeEditing/ContentTypeEditingServiceBase.cs | 4 ++-- .../Services/ContentTypeEditingServiceTests.Create.cs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs index 00708fb43912..b1847c80da0f 100644 --- a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentTypeEditing; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Strings; @@ -407,7 +407,7 @@ private static ContentTypeOperationStatus ValidateContainers(ContentTypeEditingM } // This this method gets aliases across documents, members, and media, so it covers it all - private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().Contains(alias); + private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().InvariantContains(alias); private bool ContentTypeAliasCanBeUsedFor(string alias, Guid key) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs index 814d1b5be79c..a987e9a4f78d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs @@ -861,14 +861,15 @@ public async Task Cannot_Use_Invalid_Alias(string contentTypeAlias) Assert.AreEqual(ContentTypeOperationStatus.InvalidAlias, result.Status); } - [Test] - public async Task Cannot_Use_Existing_Alias() + [TestCase("test")] // Matches alias case sensitively. + [TestCase("Test")] // Matches alias case insensitively. + public async Task Cannot_Use_Existing_Alias(string newAlias) { var createModel = ContentTypeCreateModel("Test", "test"); var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); - createModel = ContentTypeCreateModel("Test 2", "test"); + createModel = ContentTypeCreateModel("Test 2", newAlias); result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DuplicateAlias, result.Status); From e71f36d816fd515109afcad935fada0a26f406b3 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 15 Oct 2025 10:06:00 +0200 Subject: [PATCH 034/155] Back Office: Fixes link to workspace root from breadcrumb trail (closes: #20455) (#20459) Fixes link to workspace root from breadcrumb trail. --- .../workspace-menu-breadcrumb.element.ts | 8 +++++++- .../workspace-variant-menu-breadcrumb.element.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts index 0fcb8deab4fe..b1cd9d327123 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts @@ -78,7 +78,13 @@ export class UmbWorkspaceBreadcrumbElement extends UmbLitElement { #getHref(structureItem: UmbStructureItemModel) { if (structureItem.isFolder) return undefined; - return `section/${this.#sectionContext?.getPathname()}/workspace/${structureItem.entityType}/edit/${structureItem.unique}`; + + let href = `section/${this.#sectionContext?.getPathname()}`; + if (structureItem.unique) { + href += `/workspace/${structureItem.entityType}/edit/${structureItem.unique}`; + } + + return href; } override render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts index 7b267e6da820..04fe73d6a8af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts @@ -115,8 +115,13 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { #getHref(structureItem: any) { if (structureItem.isFolder) return undefined; - const workspaceBasePath = `section/${this.#sectionContext?.getPathname()}/workspace/${structureItem.entityType}/edit`; - return `${workspaceBasePath}/${structureItem.unique}/${this._workspaceActiveVariantId?.toCultureString()}`; + + let href = `section/${this.#sectionContext?.getPathname()}`; + if (structureItem.unique) { + href += `/workspace/${structureItem.entityType}/edit/${structureItem.unique}/${this._workspaceActiveVariantId?.toCultureString()}`; + } + + return href; } override render() { From 4c05a114c5b582eef5ebb36e1e38312214b910b3 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 15 Oct 2025 13:10:39 +0100 Subject: [PATCH 035/155] Fixes 20476 - Changes icon to be no entry sign (#20496) --- .../documents/documents/entity-sign/is-protected/manifest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-sign/is-protected/manifest.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-sign/is-protected/manifest.ts index a9ef60b1490d..a87addb16a0a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-sign/is-protected/manifest.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-sign/is-protected/manifest.ts @@ -9,7 +9,7 @@ export const manifests: UmbExtensionManifest = { forEntityFlags: ['Umb.IsProtected'], weight: 1000, meta: { - iconName: 'icon-lock', + iconName: 'icon-block', label: 'Protected', iconColorAlias: 'red', }, From a504fd1ef88d0ce926e865a317af91ec6dd247ce Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Thu, 16 Oct 2025 08:22:11 +0200 Subject: [PATCH 036/155] Bump version to 16.3.0 --- src/Umbraco.Web.UI.Client/package-lock.json | 4 ++-- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 33947947a951..71e0a69db2a6 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@umbraco-cms/backoffice", - "version": "16.3.0-rc", + "version": "16.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@umbraco-cms/backoffice", - "version": "16.3.0-rc", + "version": "16.3.0", "license": "MIT", "workspaces": [ "./src/packages/*", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 3c831031ace3..de785cc2058b 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.3.0-rc", + "version": "16.3.0", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index cd1e47b49d76..e71c12fcd95b 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": "16.3.0-rc4", + "version": "16.3.0", "assemblyVersion": { "precision": "build" }, From ae73fb343175e7aa23e8c2551d477dacac347037 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:24:56 +0200 Subject: [PATCH 037/155] E2E: Updated acceptance tests to match changes (#20493) * Updated tests to match changes * More updates * Bumped version * Reverted change --- tests/Umbraco.Tests.AcceptanceTest/package-lock.json | 8 ++++---- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../tests/DefaultConfig/Content/Content.spec.ts | 4 ++-- .../tests/DefaultConfig/Content/ContentInfoTab.spec.ts | 2 +- .../DefaultConfig/Content/ContentWithTrueFalse.spec.ts | 2 +- .../DataType/BlockGrid/BlockGridEditor.spec.ts | 2 +- .../DataType/BlockListEditor/BlockListEditor.spec.ts | 2 +- .../tests/DefaultConfig/Media/Media.spec.ts | 1 + 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index d6c157d75fd8..87ed32ef3ab9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.40", - "@umbraco/playwright-testhelpers": "^17.0.0-beta.1", + "@umbraco/playwright-testhelpers": "^17.0.0-beta.4", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "17.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.0-beta.1.tgz", - "integrity": "sha512-EhS4j5ARDcR3tI7ArTmLuBHW+e49qyWq3Ts8ckCXvFjkPgR3u/Z5JPOIFWUZ+rTahNZi3axs3i+dVcWWA4Fyjw==", + "version": "17.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.0-beta.4.tgz", + "integrity": "sha512-+OE1A2oAdFel4myf5T/jJLuw0aLvSOUBplkUfsYFj2ACeLygfAp/MM7q2RQ+YlCym/wdF+jAqJM3g+zsKEDjaQ==", "license": "MIT", "dependencies": { "@umbraco/json-models-builders": "2.0.40", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 49cc458287de..f1a7b85fa49e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.40", - "@umbraco/playwright-testhelpers": "^17.0.0-beta.1", + "@umbraco/playwright-testhelpers": "^17.0.0-beta.4", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" 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 f4b69a1231be..edea7a59e422 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Content.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/Content.spec.ts @@ -234,8 +234,8 @@ test('can duplicate a content node to other parent', async ({umbracoApi, umbraco await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.duplicated); await umbracoUi.content.isContentInTreeVisible(contentName); await umbracoUi.content.isContentInTreeVisible(parentContentName); - await umbracoUi.content.openContentCaretButtonForName(parentContentName); - await umbracoUi.content.isChildContentInTreeVisible(parentContentName, contentName); + await umbracoUi.content.goToContentWithName(parentContentName); + await umbracoUi.content.isContentWithNameVisibleInList(contentName); // Clean await umbracoApi.document.ensureNameNotExists(parentContentName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts index 54c07aa39fcd..a27953764450 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts @@ -17,7 +17,7 @@ test.afterEach(async ({umbracoApi}) => { test('can see correct information when published', async ({umbracoApi, umbracoUi}) => { // Arrange - const notPublishContentLink = 'This item is not published'; + const notPublishContentLink = 'Not created'; const dataTypeName = 'Textstring'; const contentText = 'This is test content text'; const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts index 75e252c28cf5..f58f224b6cb4 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTrueFalse.spec.ts @@ -58,7 +58,7 @@ test('can publish content with the true/false data type', async ({umbracoApi, um expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); const contentData = await umbracoApi.document.getByName(contentName); expect(contentData.variants[0].state).toBe(expectedState); - expect(contentData.values).toEqual([]); + expect(contentData.values[0].value).toEqual(false); }); test('can toggle the true/false value in the content', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts index 54eed6a46446..e2b5d799eca5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockGrid/BlockGridEditor.spec.ts @@ -275,7 +275,7 @@ test('max can not be less than min in a block grid editor', async ({umbracoApi, // Assert await umbracoUi.dataType.isFailedStateButtonVisible(); - await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not be exceed the high value'); + await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not exceed the high value.'); const dataTypeData = await umbracoApi.dataType.getByName(blockGridEditorName); expect(dataTypeData.values[0].value.min).toBe(minAmount); // The max value should not be updated diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts index 1239425d2001..5db02e7d48d0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts @@ -173,7 +173,7 @@ test('max can not be less than min', async ({umbracoApi, umbracoUi}) => { // Assert await umbracoUi.dataType.isFailedStateButtonVisible(); const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); - await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not be exceed the high value'); + await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not exceed the high value.'); expect(dataTypeData.values[0].value.min).toBe(minAmount); // The max value should not be updated expect(dataTypeData.values[0].value.max).toBe(oldMaxAmount); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts index ab749dc20a88..e70132f3c5ea 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Media/Media.spec.ts @@ -73,6 +73,7 @@ for (const mediaFileType of mediaFileTypes) { // Assert await umbracoUi.media.waitForMediaItemToBeCreated(); + await umbracoUi.media.goToSection(ConstantHelper.sections.media); const mediaData = await umbracoApi.media.getByName(mediaFileType.fileName); const mediaUrl = await umbracoApi.media.getFullMediaUrl(mediaData.id); await umbracoUi.media.doesMediaHaveThumbnail(mediaData.id, mediaFileType.thumbnail, mediaUrl); From 369b020d9db0315ce88c6e2835509639267c3444 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Oct 2025 12:56:32 +0200 Subject: [PATCH 038/155] Explicitly flush isolated caches by key for content updates (#20519) * Explicitly flush isolated caches by key for content updates * Apply suggestions from code review --------- Co-authored-by: Andy Butland --- .../Implement/DocumentRepository.cs | 5 + .../Repositories/Implement/MediaRepository.cs | 5 + ...ontentServiceNotificationWithCacheTests.cs | 148 ++++++++++++++++++ .../MediaServiceNotificationWithCacheTests.cs | 146 +++++++++++++++++ 4 files changed, 304 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationWithCacheTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceNotificationWithCacheTests.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 80b07966357f..8cccdd924751 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1328,6 +1328,11 @@ protected override void PersistUpdatedItem(IContent entity) entity.ResetDirtyProperties(); + // We need to flush the isolated cache by key explicitly here. + // The ContentCacheRefresher does the same thing, but by the time it's invoked, custom notification handlers + // might have already consumed the cached version (which at this point is the previous version). + IsolatedCache.ClearByKey(RepositoryCacheKeys.GetKey(entity.Key)); + // troubleshooting //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) //{ diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index d08607ec092d..75dc8a3eb786 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -480,6 +480,11 @@ protected override void PersistUpdatedItem(IMedia entity) OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); entity.ResetDirtyProperties(); + + // We need to flush the isolated cache by key explicitly here. + // The MediaCacheRefresher does the same thing, but by the time it's invoked, custom notification handlers + // might have already consumed the cached version (which at this point is the previous version). + IsolatedCache.ClearByKey(RepositoryCacheKeys.GetKey(entity.Key)); } protected override void PersistDeletedItem(IMedia entity) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationWithCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationWithCacheTests.cs new file mode 100644 index 000000000000..8bfe6859e799 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationWithCacheTests.cs @@ -0,0 +1,148 @@ +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +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.Services; + +[TestFixture] +[UmbracoTest( + Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true, + Logger = UmbracoTestOptions.Logger.Console)] +internal sealed class ContentServiceNotificationWithCacheTests : UmbracoIntegrationTest +{ + private IContentType _contentType; + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + protected override void ConfigureTestServices(IServiceCollection services) + => services.AddSingleton(AppCaches.Create(Mock.Of())); + + [SetUp] + public async Task SetupTest() + { + ContentRepositoryBase.ThrowOnWarning = true; + + _contentType = ContentTypeBuilder.CreateBasicContentType(); + _contentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(_contentType, Constants.Security.SuperUserKey); + } + + [TearDown] + public void Teardown() => ContentRepositoryBase.ThrowOnWarning = false; + + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder + .AddNotificationHandler() + .AddNotificationHandler(); + + [Test] + public async Task Saving_Saved_Get_Value() + { + var createAttempt = await ContentEditingService.CreateAsync( + new ContentCreateModel + { + ContentTypeKey = _contentType.Key, + Variants = [ + new() { Name = "Initial name" } + ], + }, + Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsTrue(createAttempt.Success); + Assert.IsNotNull(createAttempt.Result.Content); + }); + + var savingWasCalled = false; + var savedWasCalled = false; + + ContentNotificationHandler.SavingContent = notification => + { + savingWasCalled = true; + + var saved = notification.SavedEntities.First(); + var documentById = ContentService.GetById(saved.Id)!; + var documentByKey = ContentService.GetById(saved.Key)!; + + Assert.Multiple(() => + { + Assert.AreEqual("Updated name", saved.Name); + Assert.AreEqual("Initial name", documentById.Name); + Assert.AreEqual("Initial name", documentByKey.Name); + }); + }; + + ContentNotificationHandler.SavedContent = notification => + { + savedWasCalled = true; + + var saved = notification.SavedEntities.First(); + var documentById = ContentService.GetById(saved.Id)!; + var documentByKey = ContentService.GetById(saved.Key)!; + + Assert.Multiple(() => + { + Assert.AreEqual("Updated name", saved.Name); + Assert.AreEqual("Updated name", documentById.Name); + Assert.AreEqual("Updated name", documentByKey.Name); + }); + }; + + try + { + var updateAttempt = await ContentEditingService.UpdateAsync( + createAttempt.Result.Content!.Key, + new ContentUpdateModel + { + Variants = [ + new() { Name = "Updated name" } + ], + }, + Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsTrue(updateAttempt.Success); + Assert.IsNotNull(updateAttempt.Result.Content); + }); + + Assert.IsTrue(savingWasCalled); + Assert.IsTrue(savedWasCalled); + } + finally + { + ContentNotificationHandler.SavingContent = null; + ContentNotificationHandler.SavedContent = null; + } + } + + internal sealed class ContentNotificationHandler : + INotificationHandler, + INotificationHandler + { + public static Action? SavingContent { get; set; } + + public static Action? SavedContent { get; set; } + + public void Handle(ContentSavedNotification notification) => SavedContent?.Invoke(notification); + + public void Handle(ContentSavingNotification notification) => SavingContent?.Invoke(notification); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceNotificationWithCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceNotificationWithCacheTests.cs new file mode 100644 index 000000000000..d4de99dabab6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceNotificationWithCacheTests.cs @@ -0,0 +1,146 @@ +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +[TestFixture] +[UmbracoTest( + Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true, + Logger = UmbracoTestOptions.Logger.Console)] +internal sealed class MediaServiceNotificationWithCacheTests : UmbracoIntegrationTest +{ + private IMediaType _mediaType; + + private IMediaTypeService MediaTypeService => GetRequiredService(); + + private IMediaService MediaService => GetRequiredService(); + + private IMediaEditingService MediaEditingService => GetRequiredService(); + + protected override void ConfigureTestServices(IServiceCollection services) + => services.AddSingleton(AppCaches.Create(Mock.Of())); + + [SetUp] + public void SetupTest() + { + ContentRepositoryBase.ThrowOnWarning = true; + + _mediaType = MediaTypeService.Get("folder") + ?? throw new ApplicationException("Could not find the \"folder\" media type"); + } + + [TearDown] + public void Teardown() => ContentRepositoryBase.ThrowOnWarning = false; + + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder + .AddNotificationHandler() + .AddNotificationHandler(); + + [Test] + public async Task Saving_Saved_Get_Value() + { + var createAttempt = await MediaEditingService.CreateAsync( + new MediaCreateModel + { + ContentTypeKey = _mediaType.Key, + Variants = [ + new() { Name = "Initial name" } + ], + }, + Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsTrue(createAttempt.Success); + Assert.IsNotNull(createAttempt.Result.Content); + }); + + var savingWasCalled = false; + var savedWasCalled = false; + + MediaNotificationHandler.SavingMedia = notification => + { + savingWasCalled = true; + + var saved = notification.SavedEntities.First(); + var documentById = MediaService.GetById(saved.Id)!; + var documentByKey = MediaService.GetById(saved.Key)!; + + Assert.Multiple(() => + { + Assert.AreEqual("Updated name", saved.Name); + Assert.AreEqual("Initial name", documentById.Name); + Assert.AreEqual("Initial name", documentByKey.Name); + }); + }; + + MediaNotificationHandler.SavedMedia = notification => + { + savedWasCalled = true; + + var saved = notification.SavedEntities.First(); + var documentById = MediaService.GetById(saved.Id)!; + var documentByKey = MediaService.GetById(saved.Key)!; + + Assert.Multiple(() => + { + Assert.AreEqual("Updated name", saved.Name); + Assert.AreEqual("Updated name", documentById.Name); + Assert.AreEqual("Updated name", documentByKey.Name); + }); + }; + + try + { + var updateAttempt = await MediaEditingService.UpdateAsync( + createAttempt.Result.Content!.Key, + new MediaUpdateModel + { + Variants = [ + new() { Name = "Updated name" } + ], + }, + Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsTrue(updateAttempt.Success); + Assert.IsNotNull(updateAttempt.Result.Content); + }); + + Assert.IsTrue(savingWasCalled); + Assert.IsTrue(savedWasCalled); + } + finally + { + MediaNotificationHandler.SavingMedia = null; + MediaNotificationHandler.SavedMedia = null; + } + } + + internal sealed class MediaNotificationHandler : + INotificationHandler, + INotificationHandler + { + public static Action? SavingMedia { get; set; } + + public static Action? SavedMedia { get; set; } + + public void Handle(MediaSavedNotification notification) => SavedMedia?.Invoke(notification); + + public void Handle(MediaSavingNotification notification) => SavingMedia?.Invoke(notification); + } +} From 271edb521432e0ddb7d34400845c820ad271f781 Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:55:43 +0200 Subject: [PATCH 039/155] News Dashboard: split into card + container, parent handles the data from the repo (#20503) * Made card element it is own reusable component and passing the data as property. * Created the umb-news-container element to handle all the priority grouping. * Added hover styles to normal-priority cards. * Removed unused variable. --- .../components/umb-news-card.element.ts | 132 ++++++++++++ .../components/umb-news-container.element.ts | 110 ++++++++++ .../umbraco-news-dashboard.element.ts | 190 +----------------- 3 files changed, 245 insertions(+), 187 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-card.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-container.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-card.element.ts new file mode 100644 index 000000000000..133f5e53c546 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-card.element.ts @@ -0,0 +1,132 @@ +import { css, customElement, html, nothing, property, unsafeHTML, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { NewsDashboardItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +@customElement('umb-news-card') +export class UmbNewsCardElement extends UmbLitElement { + @property({ type: Object }) + item!: NewsDashboardItemResponseModel; + + @property({ type: Number }) + priority: number = 3; + + #renderHeading(priority: number, text: string) { + if (priority <= 2) return html`

${text}

`; + return html`

${text}

`; + } + + override render() { + if (!this.item) return nothing; + + const isLastRow = this.priority === 3; + + const showImage = this.priority <= 2 && !!this.item.imageUrl; + + const content = html` + ${when( + showImage, + () => + this.item.imageUrl + ? html`${this.item.imageAltText` + : html``, + () => nothing, + )} +
+ ${this.#renderHeading(this.priority, this.item.header)} + ${this.item.body ? html`
${unsafeHTML(this.item.body)}
` : nothing} + ${!isLastRow && this.item.url + ? html`
+ + ${this.item.buttonText || 'Open'} + +
` + : nothing} +
+ `; + + // Last row: whole card is a link + return isLastRow + ? this.item.url + ? html` + + ${content} + + ` + : html`
${content}
` + : html`
${content}
`; + } + + static override styles = css` + :host { + display: block; + height: 100%; + } + + .card { + background: var(--uui-color-surface); + border-radius: var(--uui-border-radius, 8px); + box-shadow: var( + --uui-box-box-shadow, + var(--uui-shadow-depth-1, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)) + ); + overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; + } + + .card-img { + width: 100%; + object-fit: cover; + display: block; + } + + .card-img.placeholder { + height: 8px; + } + + .card-body { + display: flex; + flex-direction: column; + padding: var(--uui-size-space-5); + flex: 1 1 auto; + justify-content: space-between; + gap: var(--uui-size-space-3, 9px); + } + + .card-title { + margin: 0; + } + + .card-text > p { + margin: 0; + } + + .normal-priority { + display: block; + border: 1px solid var(--uui-color-divider); + border-radius: var(--uui-border-radius, 8px); + text-decoration: none; + color: inherit; + overflow: hidden; + + .card-body { + gap: 0; + } + } + .normal-priority:hover { + color: var(--uui-color-interactive-emphasis); + } + .card-actions { + align-self: end; + } + `; +} + +export default UmbNewsCardElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-news-card': UmbNewsCardElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-container.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-container.element.ts new file mode 100644 index 000000000000..1ca905f829dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-container.element.ts @@ -0,0 +1,110 @@ +import { css, customElement, html, nothing, property, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { NewsDashboardItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +import './umb-news-card.element.js'; +import { sanitizeHTML } from '@umbraco-cms/backoffice/utils'; + +@customElement('umb-news-container') +export class UmbNewsContainerElement extends UmbLitElement { + @property({ type: Array }) + items: Array = []; + + #groupItemsByPriority(items: NewsDashboardItemResponseModel[]) { + const sanitizedItems = items.map((i) => ({ + ...i, + body: i.body ? sanitizeHTML(i.body) : '', + })); + + // Separate items by priority. + const priority1 = sanitizedItems.filter((item) => item.priority === 'High'); + const priority2 = sanitizedItems.filter((item) => item.priority === 'Medium'); + const priority3 = sanitizedItems.filter((item) => item.priority === 'Normal'); + + // Group 1: First 4 items from priority 1. + const group1Items = priority1.slice(0, 4); + const overflow1 = priority1.slice(4); + + // Group 2: Overflow from priority 1 + priority 2 items (max 4 total). + const group2Items = [...overflow1, ...priority2].slice(0, 4); + const overflow2Count = overflow1.length + priority2.length - 4; + const overflow2 = overflow2Count > 0 ? [...overflow1, ...priority2].slice(4) : []; + + // Group 3: Overflow from groups 1 & 2 + priority 3 items. + const group3Items = [...overflow2, ...priority3]; + + return [ + { priority: 1, items: group1Items }, + { priority: 2, items: group2Items }, + { priority: 3, items: group3Items }, + ]; + } + + override render() { + if (!this.items?.length) return nothing; + + const groups = this.#groupItemsByPriority(this.items); + + return html` + ${repeat( + groups, + (g) => g.priority, + (g) => html` +
+ ${repeat( + g.items, + (i, idx) => i.url || i.header || idx, + (i) => html``, + )} +
+ `, + )} + `; + } + + static override styles = css` + .cards { + --cols: 4; + --gap: var(--uui-size-space-4); + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols)), 1fr)); + gap: var(--gap); + } + + .cards + .cards { + margin-top: var(--uui-size-space-5); + } + + /* For when container-type is not been assigned, not so sure about it???*/ + @media (max-width: 1200px) { + .cards { + grid-template-columns: repeat(auto-fit, minmax(2, 1fr)); + } + } + @media (max-width: 700px) { + .cards { + grid-template-columns: 1fr; + } + } + + @container dashboard (max-width: 1200px) { + .cards { + grid-template-columns: repeat(auto-fit, minmax(2, 1fr)); + } + } + @container dashboard (max-width: 700px) { + .cards { + grid-template-columns: 1fr; + } + } + `; +} + +export default UmbNewsContainerElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-news-container': UmbNewsContainerElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts index ba1ef0e957a7..bad9fc44977a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts @@ -1,32 +1,16 @@ import { UmbNewsDashboardRepository } from './repository/index.js'; -import { - css, - customElement, - html, - nothing, - repeat, - state, - unsafeHTML, - when, -} from '@umbraco-cms/backoffice/external/lit'; -import { sanitizeHTML } from '@umbraco-cms/backoffice/utils'; +import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { NewsDashboardItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -interface UmbNewsDashboardGroupedItems { - priority: number; - items: Array; -} +import './components/umb-news-container.element.js'; @customElement('umb-umbraco-news-dashboard') export class UmbUmbracoNewsDashboardElement extends UmbLitElement { @state() private _items: Array = []; - @state() - private _groupedItems: Array = []; - @state() private _loaded: boolean = false; @@ -35,40 +19,9 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement { override async firstUpdated() { const res = await this.#repo.getNewsDashboard(); this._items = res.data?.items ?? []; - this._groupedItems = this.#groupItemsByPriority(); this._loaded = true; } - #groupItemsByPriority(): Array { - const sanitizedItems = this._items.map((i) => ({ - ...i, - body: i.body ? sanitizeHTML(i.body) : '', - })); - - // Separate items by priority. - const priority1 = sanitizedItems.filter((item) => item.priority === 'High'); - const priority2 = sanitizedItems.filter((item) => item.priority === 'Medium'); - const priority3 = sanitizedItems.filter((item) => item.priority === 'Normal'); - - // Group 1: First 4 items from priority 1. - const group1Items = priority1.slice(0, 4); - const overflow1 = priority1.slice(4); - - // Group 2: Overflow from priority 1 + priority 2 items (max 4 total). - const group2Items = [...overflow1, ...priority2].slice(0, 4); - const overflow2Count = overflow1.length + priority2.length - 4; - const overflow2 = overflow2Count > 0 ? [...overflow1, ...priority2].slice(4) : []; - - // Group 3: Overflow from groups 1 & 2 + priority 3 items. - const group3Items = [...overflow2, ...priority3]; - - return [ - { priority: 1, items: group1Items }, - { priority: 2, items: group2Items }, - { priority: 3, items: group3Items }, - ]; - } - override render() { if (!this._loaded) { return html`
`; @@ -78,58 +31,7 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement { return this.#renderDefaultContent(); } - return html` - ${repeat( - this._groupedItems, - (g) => g.priority, - (g) => html` -
- ${repeat( - g.items, - (i, idx) => i.url || i.header || idx, - (i) => { - const isLastRow = g.priority === 3; - - const content = html` - ${when( - g.priority <= 2, - () => - html`${i.imageUrl - ? html`${i.imageAltText` - : html``}`, - () => nothing, - )} -
- ${g.priority <= 2 - ? html`

${i.header}

` - : html`

${i.header}

`} - ${i.body ? html`
${unsafeHTML(i.body)}
` : null} - ${!isLastRow && i.url - ? html`
- - ${i.buttonText || 'Open'} - -
` - : nothing} -
- `; - - // LAST ROW: whole card is a link - return isLastRow - ? i.url - ? html` - - ${content} - - ` - : html`
${content}
` - : html`
${content}
`; - }, - )} -
- `, - )} - `; + return html` `; } #renderDefaultContent() { @@ -234,92 +136,6 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement { margin-top: 0; margin-bottom: 0; } - - /* Grid */ - .cards { - --cols: 4; - --gap: var(--uui-size-space-4); - width: 100%; - display: grid; - grid-template-columns: repeat( - auto-fit, - minmax(calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols)), 1fr) - ); - gap: var(--gap); - } - - .cards + .cards { - margin-top: var(--uui-size-space-5); - } - - @container (max-width: 1200px) { - .cards { - grid-template-columns: repeat(auto-fit, minmax(2, 1fr)); - } - } - @container (max-width: 700px) { - .cards { - grid-template-columns: 1fr; - } - } - - /* Card */ - .card { - background: var(--uui-color-surface); - border-radius: var(--uui-border-radius, 8px); - box-shadow: var( - --uui-box-box-shadow, - var(--uui-shadow-depth-1, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)) - ); - overflow: hidden; - display: flex; - flex-direction: column; - height: 100%; - } - - .card-img { - width: 100%; - object-fit: cover; - display: block; - } - - .card-img.placeholder { - height: 8px; - } - - .card-body { - display: flex; - flex-direction: column; - padding: var(--uui-size-space-5); - flex: 1 1 auto; - justify-content: space-between; - gap: var(--uui-size-space-3, 9px); - } - - .card-title { - margin: 0; - } - - .card-text > p { - margin: 0; - } - - .normal-priority { - display: block; - border: 1px solid var(--uui-color-divider); - border-radius: var(--uui-border-radius, 8px); - text-decoration: none; - color: inherit; - overflow: hidden; - - .card-body { - gap: 0; - } - } - - .card-actions { - align-self: end; - } `, ]; } From 4a504e8c954b0408bfeefbe898b0a5ebf702968b Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:38:26 +0200 Subject: [PATCH 040/155] Extensions: Adds `@provideContext` and `@consumeContext` decorators for a better developer experience (#20510) * feat: adds first draft of a context consume decorator * feat: uses an options pattern * feat: changes approach to use `addInitializer` and `queueMicroTask` instead * feat: adds extra warning if context is consumed on disconnected controllers * feat: example implementation of consume decorator * feat: adds support for 'subscribe' * feat: initial work on provide decorator * docs: adds license to consume decorator * feat: adds support for umbraco controllers with `hostConnected` * feat: uses asPromise to handle one-time subscription instead * test: adds unit tests for consume decorator * feat: adds support for controllers through hostConnected injection * feat: adds support for controllers through hostConnected injection * test: adds unit tests for provide decorator * docs: adds more documentation around usage and adds a few warnings in console when it detects wrong usage * feat: removes unused controllerMap * docs: adds wording on standard vs legacy decorators * docs: clarifies usage around internal state * feat: adds proper return types for decorators * docs: adds more types * feat: makes element optional * feat: makes element optional * feat: uses @consume in the log viewer to showcase * chore: cleans up debug info * feat: renames to `consumeContext` and `provideContext` to stay inline with our own methods * chore: removes unneeded typings * chore: removes not needed check * chore: removes not needed check * test: adds test for rendered value * feat: splits up code into several smaller functions * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * docs: augments code example for creating a context * Update src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../consume/context-consume.decorator.test.ts | 133 ++++++++ .../consume/context-consume.decorator.ts | 289 ++++++++++++++++++ .../context-api/consume/context-consumer.ts | 10 +- .../src/libs/context-api/consume/index.ts | 1 + .../provide/context-provide.decorator.test.ts | 111 +++++++ .../provide/context-provide.decorator.ts | 258 ++++++++++++++++ .../src/libs/context-api/provide/index.ts | 1 + .../src/mocks/data/log-viewer.data.ts | 2 +- .../log-viewer-date-range-selector.element.ts | 27 +- .../log-viewer-log-level-overview.element.ts | 24 +- .../log-viewer-log-types-chart.element.ts | 23 +- ...ewer-message-templates-overview.element.ts | 25 +- ...-viewer-saved-searches-overview.element.ts | 24 +- .../overview/log-overview-view.element.ts | 31 +- ...og-viewer-log-level-filter-menu.element.ts | 23 +- .../log-viewer-messages-list.element.ts | 41 ++- .../log-viewer-polling-button.element.ts | 32 +- .../log-viewer-search-input.element.ts | 36 ++- .../views/search/log-search-view.element.ts | 24 +- 19 files changed, 959 insertions(+), 156 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consume.decorator.test.ts create mode 100644 src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consume.decorator.ts create mode 100644 src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provide.decorator.test.ts create mode 100644 src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provide.decorator.ts diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consume.decorator.test.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consume.decorator.test.ts new file mode 100644 index 000000000000..a61cb08b1ebe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consume.decorator.test.ts @@ -0,0 +1,133 @@ +import { UmbContextToken } from '../token/context-token.js'; +import type { UmbContextMinimal } from '../types.js'; +import { UmbContextProvider } from '../provide/context-provider.js'; +import { consumeContext } from './context-consume.decorator.js'; +import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { html, state } from '@umbraco-cms/backoffice/external/lit'; + +class UmbTestContextConsumerClass implements UmbContextMinimal { + public prop: string = 'value from provider'; + getHostElement() { + return undefined as any; + } +} + +const testToken = new UmbContextToken('my-test-context'); + +class MyTestElement extends UmbLitElement { + @consumeContext({ + context: testToken, + }) + @state() + contextValue?: UmbTestContextConsumerClass; + + override render() { + return html`
${this.contextValue?.prop ?? 'no context'}
`; + } +} + +customElements.define('my-consume-test-element', MyTestElement); + +describe('@consume decorator', () => { + let provider: UmbContextProvider; + let element: MyTestElement; + + beforeEach(async () => { + provider = new UmbContextProvider(document.body, testToken, new UmbTestContextConsumerClass()); + provider.hostConnected(); + + element = await fixture(``); + }); + + afterEach(() => { + provider.destroy(); + (provider as any) = undefined; + }); + + it('should receive a context value when provided on the host', () => { + expect(element.contextValue).to.equal(provider.providerInstance()); + expect(element.contextValue?.prop).to.equal('value from provider'); + }); + + it('should render the value from the context', async () => { + expect(element).shadowDom.to.equal('
value from provider
'); + }); + + it('should work when the decorator is used in a controller', async () => { + class MyController extends UmbControllerBase { + @consumeContext({ context: testToken }) + contextValue?: UmbTestContextConsumerClass; + } + + const controller = new MyController(element); + + await elementUpdated(element); + + expect(element.contextValue).to.equal(provider.providerInstance()); + expect(controller.contextValue).to.equal(provider.providerInstance()); + }); + + it('should have called the callback first', async () => { + let callbackCalled = false; + + class MyCallbackTestElement extends UmbLitElement { + @consumeContext({ + context: testToken, + callback: () => { + callbackCalled = true; + }, + }) + contextValue?: UmbTestContextConsumerClass; + } + + customElements.define('my-callback-consume-test-element', MyCallbackTestElement); + + const callbackElement = await fixture( + ``, + ); + + await elementUpdated(callbackElement); + + expect(callbackCalled).to.be.true; + expect(callbackElement.contextValue).to.equal(provider.providerInstance()); + }); + + it('should update the context value when the provider instance changes', async () => { + const newProviderInstance = new UmbTestContextConsumerClass(); + newProviderInstance.prop = 'new value from provider'; + + const newProvider = new UmbContextProvider(element, testToken, newProviderInstance); + newProvider.hostConnected(); + + await elementUpdated(element); + + expect(element.contextValue).to.equal(newProvider.providerInstance()); + expect(element.contextValue?.prop).to.equal(newProviderInstance.prop); + }); + + it('should be able to consume without subscribing', async () => { + class MyNoSubscribeTestController extends UmbControllerBase { + @consumeContext({ context: testToken, subscribe: false }) + contextValue?: UmbTestContextConsumerClass; + } + + const controller = new MyNoSubscribeTestController(element); + await aTimeout(0); // Wait a tick for promise to resolve + + expect(controller.contextValue).to.equal(provider.providerInstance()); + + const newProviderInstance = new UmbTestContextConsumerClass(); + newProviderInstance.prop = 'new value from provider'; + + const newProvider = new UmbContextProvider(element, testToken, newProviderInstance); + newProvider.hostConnected(); + + await aTimeout(0); // Wait a tick for promise to resolve + + // Should still be the old value + expect(controller.contextValue).to.not.equal(newProvider.providerInstance()); + expect(controller.contextValue?.prop).to.equal('value from provider'); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consume.decorator.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consume.decorator.ts new file mode 100644 index 000000000000..717c460d93f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consume.decorator.ts @@ -0,0 +1,289 @@ +/* + * Portions of this code are adapted from @lit/context + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + * + * Original source: https://github.com/lit/lit/tree/main/packages/context + * + * @license BSD-3-Clause + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { UmbContextToken } from '../token/index.js'; +import type { UmbContextMinimal } from '../types.js'; +import { UmbContextConsumerController } from './context-consumer.controller.js'; +import type { UmbContextCallback } from './context-request.event.js'; + +export interface UmbConsumeOptions< + BaseType extends UmbContextMinimal = UmbContextMinimal, + ResultType extends BaseType = BaseType, +> { + /** + * The context to consume, either as a string alias or an UmbContextToken. + */ + context: string | UmbContextToken; + + /** + * An optional callback that is invoked when the context value is set or changes. + * Note, the class instance is probably not fully constructed when this is first invoked. + * If you need to ensure the class is fully constructed, consider using a setter on the property instead. + */ + callback?: UmbContextCallback; + + /** + * If true, the context consumer will stay active and invoke the callback on context changes. + * If false, the context consumer will use asPromise() to get the value once and then clean up. + * @default true + */ + subscribe?: boolean; +} + +/** + * A property decorator that adds an UmbContextConsumerController to the component + * which will try and retrieve a value for the property via the Umbraco Context API. + * + * This decorator supports both modern "standard" decorators (Stage 3 TC39 proposal) and + * legacy TypeScript experimental decorators for backward compatibility. + * + * @param {UmbConsumeOptions} options Configuration object containing context, callback, and subscribe options + * + * @example + * ```ts + * import {consumeContext} from '@umbraco-cms/backoffice/context-api'; + * import {UMB_WORKSPACE_CONTEXT} from './workspace.context-token.js'; + * + * class MyElement extends UmbLitElement { + * // Standard decorators (with 'accessor' keyword) - Modern approach + * @consumeContext({context: UMB_WORKSPACE_CONTEXT}) + * accessor workspaceContext?: UmbWorkspaceContext; + * + * // Legacy decorators (without 'accessor') - Works with @state/@property + * @consumeContext({context: UMB_USER_CONTEXT, subscribe: false}) + * @state() + * currentUser?: UmbUserContext; + * } + * ``` + * @returns {ConsumeDecorator} A property decorator function + */ +export function consumeContext< + BaseType extends UmbContextMinimal = UmbContextMinimal, + ResultType extends BaseType = BaseType, +>(options: UmbConsumeOptions): ConsumeDecorator { + const { context, callback, subscribe = true } = options; + + return ((protoOrTarget: any, nameOrContext: PropertyKey | ClassAccessorDecoratorContext) => { + if (typeof nameOrContext === 'object') { + setupStandardDecorator(protoOrTarget, nameOrContext, context, callback, subscribe); + return; + } + + setupLegacyDecorator(protoOrTarget, nameOrContext as string, context, callback, subscribe); + }) as ConsumeDecorator; +} + +/** + * Sets up a standard decorator (Stage 3 TC39 proposal) for auto-accessors. + * This branch is used when decorating with the 'accessor' keyword. + * Example: @consumeContext({context: TOKEN}) accessor myProp?: Type; + * + * The decorator receives a ClassAccessorDecoratorContext object which provides + * addInitializer() to run code during class construction. + * + * This is the modern, standardized decorator API that will be the standard + * when Lit 4.x is released. + * + * Note: Standard decorators currently don't work with @state()/@property() + * decorators, which is why we still need the legacy branch. + */ +function setupStandardDecorator( + protoOrTarget: any, + decoratorContext: ClassAccessorDecoratorContext, + context: string | UmbContextToken, + callback: UmbContextCallback | undefined, + subscribe: boolean, +): void { + if (!('addInitializer' in decoratorContext)) { + console.warn( + '@consumeContext decorator: Standard decorator context does not support addInitializer. ' + + 'This should not happen with modern decorators.', + ); + return; + } + + decoratorContext.addInitializer(function () { + queueMicrotask(() => { + if (subscribe) { + // Continuous subscription - stays active and updates property on context changes + new UmbContextConsumerController(this, context, (value) => { + protoOrTarget.set.call(this, value); + callback?.(value); + }); + } else { + // One-time consumption - uses asPromise() to get the value once and then cleans up + const controller = new UmbContextConsumerController(this, context, callback); + controller.asPromise().then((value) => { + protoOrTarget.set.call(this, value); + }); + } + }); + }); +} + +/** + * Sets up a legacy decorator (TypeScript experimental) for regular properties. + * This branch is used when decorating without the 'accessor' keyword. + * Example: @consumeContext({context: TOKEN}) @state() myProp?: Type; + * + * The decorator receives: + * - protoOrTarget: The class prototype + * - propertyKey: The property name (string) + * + * This is the older TypeScript experimental decorator API, still widely used + * in Umbraco because it works with @state() and @property() decorators. + * The 'accessor' keyword is not compatible with these decorators yet. + * + * We support three initialization strategies: + * 1. addInitializer (if available, e.g., on LitElement classes) + * 2. hostConnected wrapper (for UmbController classes) + * 3. Warning (if neither is available) + */ +function setupLegacyDecorator( + protoOrTarget: any, + propertyKey: string, + context: string | UmbContextToken, + callback: UmbContextCallback | undefined, + subscribe: boolean, +): void { + const constructor = protoOrTarget.constructor as any; + + // Strategy 1: Use addInitializer if available (LitElement classes) + if (constructor.addInitializer) { + constructor.addInitializer((element: any): void => { + queueMicrotask(() => { + if (subscribe) { + // Continuous subscription + new UmbContextConsumerController(element, context, (value) => { + element[propertyKey] = value; + callback?.(value); + }); + } else { + // One-time consumption using asPromise() + const controller = new UmbContextConsumerController(element, context, callback); + controller.asPromise().then((value) => { + element[propertyKey] = value; + }); + } + }); + }); + return; + } + + // Strategy 2: Wrap hostConnected for UmbController classes without addInitializer + if ('hostConnected' in protoOrTarget && typeof protoOrTarget.hostConnected === 'function') { + const originalHostConnected = protoOrTarget.hostConnected; + + protoOrTarget.hostConnected = function (this: any) { + // Set up consumer once, using a flag to prevent multiple setups + if (!this.__consumeControllers) { + this.__consumeControllers = new Map(); + } + + if (!this.__consumeControllers.has(propertyKey)) { + if (subscribe) { + // Continuous subscription + const controller = new UmbContextConsumerController(this, context, (value) => { + this[propertyKey] = value; + callback?.(value); + }); + this.__consumeControllers.set(propertyKey, controller); + } else { + // One-time consumption using asPromise() + const controller = new UmbContextConsumerController(this, context, callback); + controller.asPromise().then((value) => { + this[propertyKey] = value; + }); + // Don't store in map since it cleans itself up + } + } + + // Call original hostConnected if it exists + originalHostConnected?.call(this); + }; + return; + } + + // Strategy 3: No supported initialization method available + console.warn( + `@consumeContext applied to ${constructor.name}.${propertyKey} but neither addInitializer nor hostConnected is available. ` + + `Make sure the class extends UmbLitElement, UmbControllerBase, or implements UmbController with hostConnected.`, + ); +} + +/** + * Generates a public interface type that removes private and protected fields. + * This allows accepting otherwise incompatible versions of the type (e.g. from + * multiple copies of the same package in `node_modules`). + */ +type Interface = { + [K in keyof T]: T[K]; +}; + +declare class ReactiveElement { + static addInitializer?: (initializer: (instance: any) => void) => void; +} + +declare class ReactiveController { + hostConnected?: () => void; +} + +/** + * A type representing the base class of which the decorator should work + * requiring either addInitializer (UmbLitElement) or hostConnected (UmbController). + */ +type ReactiveEntity = ReactiveElement | ReactiveController; + +type ConsumeDecorator = { + // legacy + >( + protoOrDescriptor: Proto, + name?: K, + ): FieldMustMatchProvidedType; + + // standard + , V extends ValueType>( + value: ClassAccessorDecoratorTarget, + context: ClassAccessorDecoratorContext, + ): void; +}; + +// Note TypeScript requires the return type of a decorator to be `void | any` +type DecoratorReturn = void | any; + +type FieldMustMatchProvidedType = + // First we check whether the object has the property as a required field + Obj extends Record + ? // Ok, it does, just check whether it's ok to assign the + // provided type to the consuming field + [ProvidedType] extends [ConsumingType] + ? DecoratorReturn + : { + message: 'provided type not assignable to consuming field'; + provided: ProvidedType; + consuming: ConsumingType; + } + : // Next we check whether the object has the property as an optional field + Obj extends Partial> + ? // Check assignability again. Note that we have to include undefined + // here on the consuming type because it's optional. + [ProvidedType] extends [ConsumingType | undefined] + ? DecoratorReturn + : { + message: 'provided type not assignable to consuming field'; + provided: ProvidedType; + consuming: ConsumingType | undefined; + } + : // Ok, the field isn't present, so either someone's using consume + // manually, i.e. not as a decorator (maybe don't do that! but if you do, + // you're on your own for your type checking, sorry), or the field is + // private, in which case we can't check it. + DecoratorReturn; diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts index bcc6ded36097..4372b6937792 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts @@ -191,13 +191,21 @@ export class UmbContextConsumer< cancelAnimationFrame(this.#raf); } + const hostElement = this._retrieveHost(); + + // Add connection check to prevent requesting on disconnected elements + if (hostElement && !hostElement.isConnected) { + console.warn('UmbContextConsumer: Attempting to request context on disconnected element', hostElement); + return; + } + const event = new UmbContextRequestEventImplementation( this.#contextAlias, this.#apiAlias, this._onResponse, this.#stopAtContextMatch, ); - (this.#skipHost ? this._retrieveHost()?.parentNode : this._retrieveHost())?.dispatchEvent(event); + (this.#skipHost ? hostElement?.parentNode : hostElement)?.dispatchEvent(event); if (this.#promiseResolver && this.#promiseOptions?.preventTimeout !== true) { this.#raf = requestAnimationFrame(() => { diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/index.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/index.ts index bc1302658ce4..0f2ec13c2050 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/index.ts @@ -1,3 +1,4 @@ +export * from './context-consume.decorator.js'; export * from './context-consumer.controller.js'; export * from './context-consumer.js'; export * from './context-request.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provide.decorator.test.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provide.decorator.test.ts new file mode 100644 index 000000000000..9965e827237b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provide.decorator.test.ts @@ -0,0 +1,111 @@ +import { UmbContextToken } from '../token/context-token.js'; +import type { UmbContextMinimal } from '../types.js'; +import { provideContext } from './context-provide.decorator.js'; +import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +class UmbTestContextConsumerClass implements UmbContextMinimal { + public prop: string; + + constructor(initialValue = 'value from provider') { + this.prop = initialValue; + } + + getHostElement() { + return document.body; + } +} + +const testToken = new UmbContextToken('my-test-context', 'testApi'); + +class MyTestRootElement extends UmbLitElement { + @provideContext({ context: testToken }) + providerInstance = new UmbTestContextConsumerClass(); +} + +customElements.define('my-provide-test-element', MyTestRootElement); + +class MyTestElement extends UmbLitElement { + contextValue?: UmbTestContextConsumerClass; + + constructor() { + super(); + + this.consumeContext(testToken, (value) => { + this.contextValue = value; + }); + } +} + +customElements.define('my-consume-test-element', MyTestElement); + +describe('@provide decorator', () => { + let rootElement: MyTestRootElement; + let element: MyTestElement; + + beforeEach(async () => { + rootElement = await fixture( + ``, + ); + element = rootElement.querySelector('my-consume-test-element') as MyTestElement; + }); + + afterEach(() => {}); + + it('should receive a context value when provided on the host', () => { + expect(element.contextValue).to.equal(rootElement.providerInstance); + }); + + it('should work when the decorator is used in a controller', async () => { + class MyController extends UmbControllerBase { + @provideContext({ context: testToken }) + providerInstance = new UmbTestContextConsumerClass('new value'); + } + + const controller = new MyController(element); + + await elementUpdated(element); + + expect(element.contextValue).to.equal(controller.providerInstance); + expect(controller.providerInstance.prop).to.equal('new value'); + }); + + it('should not update the instance when the property changes', async () => { + // we do not support setting a new value on a provided property + // as it would require a lot more logic to handle updating the context consumers + // So for now we just warn the user that this is not supported + // This might be revisited in the future if there is a need for it + + const originalProviderInstance = rootElement.providerInstance; + + const newProviderInstance = new UmbTestContextConsumerClass('new value from provider'); + rootElement.providerInstance = newProviderInstance; + + await aTimeout(0); + + expect(element.contextValue).to.equal(originalProviderInstance); + expect(element.contextValue?.prop).to.equal(originalProviderInstance.prop); + }); + + it('should update the context value when the provider instance is replaced', async () => { + const newProviderInstance = new UmbTestContextConsumerClass(); + newProviderInstance.prop = 'new value from provider'; + + class MyUpdateTestElement extends UmbLitElement { + @provideContext({ context: testToken }) + providerInstance = newProviderInstance; + } + customElements.define('my-update-provide-test-element', MyUpdateTestElement); + + const newProvider = await fixture( + ``, + ); + const element = newProvider.querySelector('my-consume-test-element') as MyTestElement; + + await elementUpdated(element); + + expect(element.contextValue).to.equal(newProviderInstance); + expect(element.contextValue?.prop).to.equal(newProviderInstance.prop); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provide.decorator.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provide.decorator.ts new file mode 100644 index 000000000000..621009856209 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provide.decorator.ts @@ -0,0 +1,258 @@ +/* + * Portions of this code are adapted from @lit/context + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + * + * Original source: https://github.com/lit/lit/tree/main/packages/context + * + * @license BSD-3-Clause + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { UmbContextToken } from '../token/index.js'; +import type { UmbContextMinimal } from '../types.js'; +import { UmbContextProviderController } from './context-provider.controller.js'; + +export interface UmbProvideOptions { + context: string | UmbContextToken; +} + +/** + * A property decorator that creates an UmbContextProviderController to provide + * a context value to child elements via the Umbraco Context API. + * + * This decorator supports both modern "standard" decorators (Stage 3 TC39 proposal) and + * legacy TypeScript experimental decorators for backward compatibility. + * + * The provider is created once during initialization with the property's initial value. + * To update the provided value dynamically, keep a state inside the provided context instance + * and update that state as needed. The context instance itself should remain the same. + * You can use any of the Umb{*}State classes. + * + * @param {UmbProvideOptions} options Configuration object containing the context token + * + * @example + * ```ts + * import {provideContext} from '@umbraco-cms/backoffice/context-api'; + * import {UMB_WORKSPACE_CONTEXT} from './workspace.context-token.js'; + * + * class MyWorkspaceElement extends UmbLitElement { + * // Standard decorators - requires 'accessor' keyword + * @provideContext({context: UMB_WORKSPACE_CONTEXT}) + * accessor workspaceContext = new UmbWorkspaceContext(this); + * + * // Legacy decorators - works without 'accessor' + * @provideContext({context: UMB_WORKSPACE_CONTEXT}) + * workspaceContext = new UmbWorkspaceContext(this); + * } + * ``` + * + * @example + * ```ts + * // For dynamic updates, store the state inside the context instance + * class MyContext extends UmbControllerBase { + * someProperty = new UmbStringState('initial value'); + * } + * + * class MyElement extends UmbLitElement { + * @provideContext({context: MY_CONTEXT}) + * private _myContext = new MyContext(this); + * + * updateValue(newValue: string) { + * this._myContext.someProperty.setValue(newValue); + * } + * } + * ``` + * + * @returns {ProvideDecorator} A property decorator function + */ +export function provideContext< + BaseType extends UmbContextMinimal = UmbContextMinimal, + ResultType extends BaseType = BaseType, + InstanceType extends ResultType = ResultType, +>(options: UmbProvideOptions): ProvideDecorator { + const { context } = options; + + return (( + protoOrTarget: any, + nameOrContext: PropertyKey | ClassAccessorDecoratorContext, + ): void | any => { + if (typeof nameOrContext === 'object') { + return setupStandardDecorator(protoOrTarget, context); + } + + setupLegacyDecorator(protoOrTarget, nameOrContext as string, context); + }) as ProvideDecorator; +} + +/** + * Sets up a standard decorator (Stage 3 TC39 proposal) for auto-accessors. + * This branch is used when decorating with the 'accessor' keyword. + * Example: @provideContext({context: TOKEN}) accessor myProp = new MyContext(); + * + * The decorator receives a ClassAccessorDecoratorContext object and returns + * an accessor descriptor that intercepts the property initialization. + * + * This is the modern, standardized decorator API that will be the standard + * when Lit 4.x is released. + * + * Note: Standard decorators currently don't work with @state()/@property() + * decorators, which is why we still need the legacy branch. + */ +function setupStandardDecorator< + BaseType extends UmbContextMinimal, + ResultType extends BaseType, + InstanceType extends ResultType, +>(protoOrTarget: any, context: string | UmbContextToken) { + return { + get(this: any) { + return protoOrTarget.get.call(this); + }, + set(this: any, value: InstanceType) { + return protoOrTarget.set.call(this, value); + }, + init(this: any, value: InstanceType) { + // Defer controller creation to avoid timing issues with private fields + queueMicrotask(() => { + new UmbContextProviderController(this, context, value); + }); + return value; + }, + }; +} + +/** + * Sets up a legacy decorator (TypeScript experimental) for regular properties. + * This branch is used when decorating without the 'accessor' keyword. + * Example: @provideContext({context: TOKEN}) myProp = new MyContext(); + * + * The decorator receives: + * - protoOrTarget: The class prototype + * - propertyKey: The property name (string) + * + * This is the older TypeScript experimental decorator API, still widely used + * in Umbraco because it works with @state() and @property() decorators. + * The 'accessor' keyword is not compatible with these decorators yet. + * + * We support three initialization strategies: + * 1. addInitializer (if available, e.g., on LitElement classes) + * 2. hostConnected wrapper (for UmbController classes) + * 3. Warning (if neither is available) + */ +function setupLegacyDecorator< + BaseType extends UmbContextMinimal, + ResultType extends BaseType, + InstanceType extends ResultType, +>(protoOrTarget: any, propertyKey: string, context: string | UmbContextToken): void { + const constructor = protoOrTarget.constructor as any; + + // Strategy 1: Use addInitializer if available (LitElement classes) + if (constructor.addInitializer) { + constructor.addInitializer((element: any): void => { + // Defer controller creation to avoid timing issues with private fields + queueMicrotask(() => { + const initialValue = element[propertyKey]; + new UmbContextProviderController(element, context, initialValue); + }); + }); + return; + } + + // Strategy 2: Wrap hostConnected for UmbController classes without addInitializer + if ('hostConnected' in protoOrTarget && typeof protoOrTarget.hostConnected === 'function') { + const originalHostConnected = protoOrTarget.hostConnected; + + protoOrTarget.hostConnected = function (this: any) { + // Set up provider once, using a flag to prevent multiple setups + if (!this.__provideControllers) { + this.__provideControllers = new Map(); + } + + if (!this.__provideControllers.has(propertyKey)) { + const initialValue = this[propertyKey]; + new UmbContextProviderController(this, context, initialValue); + // Mark as set up to prevent duplicate providers + this.__provideControllers.set(propertyKey, true); + } + + // Call original hostConnected if it exists + originalHostConnected?.call(this); + }; + return; + } + + // Strategy 3: No supported initialization method available + console.warn( + `@provideContext applied to ${constructor.name}.${propertyKey} but neither addInitializer nor hostConnected is available. ` + + `Make sure the class extends UmbLitElement, UmbControllerBase, or implements UmbController with hostConnected.`, + ); +} + +/** + * Generates a public interface type that removes private and protected fields. + * This allows accepting otherwise compatible versions of the type (e.g. from + * multiple copies of the same package in `node_modules`). + */ +type Interface = { + [K in keyof T]: T[K]; +}; + +declare class ReactiveElement { + static addInitializer?: (initializer: (instance: any) => void) => void; +} + +declare class ReactiveController { + hostConnected?: () => void; +} + +/** + * A type representing the base class of which the decorator should work + * requiring either addInitializer (UmbLitElement) or hostConnected (UmbController). + */ +type ReactiveEntity = ReactiveElement | ReactiveController; + +type ProvideDecorator = { + // legacy + >( + protoOrDescriptor: Proto, + name?: K, + ): FieldMustMatchContextType; + + // standard + , V extends ContextType>( + value: ClassAccessorDecoratorTarget, + context: ClassAccessorDecoratorContext, + ): void; +}; + +// Note TypeScript requires the return type of a decorator to be `void | any` +type DecoratorReturn = void | any; + +type FieldMustMatchContextType = + // First we check whether the object has the property as a required field + Obj extends Record + ? // Ok, it does, just check whether it's ok to assign the + // provided type to the consuming field + [ProvidingType] extends [ContextType] + ? DecoratorReturn + : { + message: 'providing field not assignable to context'; + context: ContextType; + provided: ProvidingType; + } + : // Next we check whether the object has the property as an optional field + Obj extends Partial> + ? // Check assignability again. Note that we have to include undefined + // here on the providing type because it's optional. + [Providing | undefined] extends [ContextType] + ? DecoratorReturn + : { + message: 'providing field not assignable to context'; + context: ContextType; + consuming: Providing | undefined; + } + : // Ok, the field isn't present, so either someone's using provide + // manually, i.e. not as a decorator (maybe don't do that! but if you do, + // you're on your own for your type checking, sorry), or the field is + // private, in which case we can't check it. + DecoratorReturn; diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/index.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/index.ts index 3690c440fd8e..ecad303f1961 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/index.ts @@ -1,3 +1,4 @@ +export * from './context-provide.decorator.js'; export * from './context-boundary.js'; export * from './context-boundary.controller.js'; export * from './context-provide.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts index fa179f05e791..96872fa203a9 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/log-viewer.data.ts @@ -44,7 +44,7 @@ class UmbLogViewerMessagesData extends UmbMockDBBase { } getLevelCount() { - const levels = this.data.map((log) => log.level ?? 'unknown'); + const levels = this.data.map((log) => log.level?.toLowerCase() ?? 'unknown'); const counts = {}; levels.forEach((level: string) => { //eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts index e07e40afd4e4..71d6c6c8440e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/components/log-viewer-date-range-selector.element.ts @@ -1,10 +1,11 @@ -import type { UmbLogViewerDateRange, UmbLogViewerWorkspaceContext } from '../workspace/logviewer-workspace.context.js'; +import type { UmbLogViewerDateRange } from '../workspace/logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../workspace/logviewer-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { query as getQuery, path, toQueryString } from '@umbraco-cms/backoffice/router'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-date-range-selector') export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { @@ -17,20 +18,20 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { @property({ type: Boolean, reflect: true }) horizontal = false; - #logViewerContext?: UmbLogViewerWorkspaceContext; + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#observeStuff(); - }); + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#observeStuff(); + } + private get _logViewerContext() { + return this.#logViewerContext; } #observeStuff() { - if (!this.#logViewerContext) return; this.observe( - this.#logViewerContext.dateRange, + this._logViewerContext?.dateRange, (dateRange: UmbLogViewerDateRange) => { this._startDate = dateRange.startDate; this._endDate = dateRange.endDate; @@ -50,7 +51,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { } #updateFiltered() { - this.#logViewerContext?.setDateRange({ startDate: this._startDate, endDate: this._endDate }); + this._logViewerContext?.setDateRange({ startDate: this._startDate, endDate: this._endDate }); const query = getQuery(); const qs = toQueryString({ @@ -71,7 +72,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { id="start-date" type="date" label="From" - .max=${this.#logViewerContext?.today ?? ''} + .max=${this._logViewerContext?.today ?? ''} .value=${this._startDate}>
@@ -82,7 +83,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement { type="date" label="To" .min=${this._startDate} - .max=${this.#logViewerContext?.today ?? ''} + .max=${this._logViewerContext?.today ?? ''} .value=${this._endDate}>
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-level-overview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-level-overview.element.ts index ee03bf2a66d3..4fff8dedcc6a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-level-overview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-level-overview.element.ts @@ -1,23 +1,26 @@ -import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; import { html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { LoggerResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-log-level-overview') export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement { - #logViewerContext?: UmbLogViewerWorkspaceContext; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#logViewerContext?.getSavedSearches(); - this.#observeLogLevels(); - }); + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; + + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#logViewerContext?.getSavedSearches(); + this.#observeLogLevels(); + } + private get _logViewerContext() { + return this.#logViewerContext; } @state() private _loggers: LoggerResponseModel[] = []; + /** * The name of the logger to get the level for. Defaults to 'Global'. * @memberof UmbLogViewerLogLevelOverviewElement @@ -26,8 +29,7 @@ export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement { loggerName = 'Global'; #observeLogLevels() { - if (!this.#logViewerContext) return; - this.observe(this.#logViewerContext.loggers, (loggers) => { + this.observe(this._logViewerContext?.loggers, (loggers) => { this._loggers = loggers ?? []; }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts index fbc61d9b126f..c314fa23e6c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-log-types-chart.element.ts @@ -1,19 +1,21 @@ -import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { LogLevelCountsReponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-log-types-chart') export class UmbLogViewerLogTypesChartElement extends UmbLitElement { - #logViewerContext?: UmbLogViewerWorkspaceContext; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#logViewerContext?.getLogCount(); - this.#observeStuff(); - }); + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; + + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#logViewerContext?.getLogCount(); + this.#observeStuff(); + } + private get _logViewerContext() { + return this.#logViewerContext; } @state() @@ -47,8 +49,7 @@ export class UmbLogViewerLogTypesChartElement extends UmbLitElement { } #observeStuff() { - if (!this.#logViewerContext) return; - this.observe(this.#logViewerContext.logCount, (logLevel) => { + this.observe(this._logViewerContext?.logCount, (logLevel) => { this._logLevelCountResponse = logLevel ?? null; this.setLogLevelCount(); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-message-templates-overview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-message-templates-overview.element.ts index 41dfc298c6c6..35c00a5fe501 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-message-templates-overview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-message-templates-overview.element.ts @@ -1,10 +1,10 @@ -import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { LogTemplateResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-message-templates-overview') export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement { @@ -17,19 +17,20 @@ export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement { @state() private _messageTemplates: Array = []; - #logViewerContext?: UmbLogViewerWorkspaceContext; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#logViewerContext?.getMessageTemplates(0, this.#itemsPerPage); - this.#observeStuff(); - }); + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; + + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#getMessageTemplates(); + this.#observeStuff(); + } + private get _logViewerContext() { + return this.#logViewerContext; } #observeStuff() { - if (!this.#logViewerContext) return; - this.observe(this.#logViewerContext.messageTemplates, (templates) => { + this.observe(this._logViewerContext?.messageTemplates, (templates) => { this._messageTemplates = templates?.items ?? []; this._total = templates?.total ?? 0; }); @@ -37,7 +38,7 @@ export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement { #getMessageTemplates() { const skip = this.#currentPage * this.#itemsPerPage - this.#itemsPerPage; - this.#logViewerContext?.getMessageTemplates(skip, this.#itemsPerPage); + this._logViewerContext?.getMessageTemplates(skip, this.#itemsPerPage); } #onChangePage(event: UUIPaginationEvent) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-saved-searches-overview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-saved-searches-overview.element.ts index beb1753f6191..226a3676f06b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-saved-searches-overview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/components/log-viewer-saved-searches-overview.element.ts @@ -1,10 +1,10 @@ -import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { SavedLogSearchResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-saved-searches-overview') export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement { @@ -17,20 +17,20 @@ export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement { @state() private _total = 0; - #logViewerContext?: UmbLogViewerWorkspaceContext; + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#logViewerContext?.getSavedSearches({ skip: 0, take: this.#itemsPerPage }); - this.#observeStuff(); - }); + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#getSavedSearches(); + this.#observeStuff(); + } + private get _logViewerContext() { + return this.#logViewerContext; } #observeStuff() { - if (!this.#logViewerContext) return; - this.observe(this.#logViewerContext.savedSearches, (savedSearches) => { + this.observe(this._logViewerContext?.savedSearches, (savedSearches) => { this._savedSearches = savedSearches?.items ?? []; this._total = savedSearches?.total ?? 0; }); @@ -38,7 +38,7 @@ export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement { #getSavedSearches() { const skip = this.#currentPage * this.#itemsPerPage - this.#itemsPerPage; - this.#logViewerContext?.getSavedSearches({ skip, take: this.#itemsPerPage }); + this._logViewerContext?.getSavedSearches({ skip, take: this.#itemsPerPage }); } #onChangePage(event: UUIPaginationEvent) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/log-overview-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/log-overview-view.element.ts index 19ebd64195c0..1c0ec1b9711b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/log-overview-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/overview/log-overview-view.element.ts @@ -1,7 +1,7 @@ -import type { UmbLogViewerWorkspaceContext } from '../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../logviewer-workspace.context-token.js'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; //TODO: add a disabled attribute to the show more button when the total number of items is correctly returned from the endpoint @customElement('umb-log-viewer-overview-view') @@ -12,28 +12,29 @@ export class UmbLogViewerOverviewViewElement extends UmbLitElement { @state() private _canShowLogs = false; - #logViewerContext?: UmbLogViewerWorkspaceContext; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#observeErrorCount(); - this.#observeCanShowLogs(); - this.#logViewerContext?.getLogLevels(0, 100); - }); + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; + + @consumeContext({ + context: UMB_APP_LOG_VIEWER_CONTEXT, + }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#observeErrorCount(); + this.#observeCanShowLogs(); + value?.getLogLevels(0, 100); + } + private get _logViewerContext() { + return this.#logViewerContext; } #observeErrorCount() { - if (!this.#logViewerContext) return; - - this.observe(this.#logViewerContext.logCount, (logLevelCount) => { + this.observe(this._logViewerContext?.logCount, (logLevelCount) => { this._errorCount = logLevelCount?.error; }); } #observeCanShowLogs() { - if (!this.#logViewerContext) return; - this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => { + this.observe(this._logViewerContext?.canShowLogs, (canShowLogs) => { this._canShowLogs = canShowLogs ?? false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-log-level-filter-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-log-level-filter-menu.element.ts index 1db43e7f7e1a..f2a69a2d9be8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-log-level-filter-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-log-level-filter-menu.element.ts @@ -1,4 +1,3 @@ -import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; import type { UUICheckboxElement } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, queryAll, state } from '@umbraco-cms/backoffice/external/lit'; @@ -6,6 +5,7 @@ import { debounce } from '@umbraco-cms/backoffice/utils'; import { LogLevelModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { path, query, toQueryString } from '@umbraco-cms/backoffice/router'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-log-level-filter-menu') export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement { @@ -15,27 +15,24 @@ export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement { @state() private _logLevelFilter: LogLevelModel[] = []; - #logViewerContext?: UmbLogViewerWorkspaceContext; + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#observeLogLevelFilter(); - }); + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#observeLogLevelFilter(); + } + private get _logViewerContext() { + return this.#logViewerContext; } #observeLogLevelFilter() { - if (!this.#logViewerContext) return; - - this.observe(this.#logViewerContext.logLevelsFilter, (levelsFilter) => { + this.observe(this._logViewerContext?.logLevelsFilter, (levelsFilter) => { this._logLevelFilter = levelsFilter ?? []; }); } #setLogLevel() { - if (!this.#logViewerContext) return; - const logLevels = Array.from(this._logLevelSelectorCheckboxes) .filter((checkbox) => checkbox.checked) .map((checkbox) => checkbox.value as LogLevelModel); diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-messages-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-messages-list.element.ts index c404cc622ec8..3e1bed642ee1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-messages-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-messages-list.element.ts @@ -1,10 +1,10 @@ -import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; import type { UUIScrollContainerElement, UUIPaginationElement } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { LogMessageResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DirectionModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-messages-list') export class UmbLogViewerMessagesListElement extends UmbLitElement { @@ -23,46 +23,45 @@ export class UmbLogViewerMessagesListElement extends UmbLitElement { @state() private _isLoading = true; - #logViewerContext?: UmbLogViewerWorkspaceContext; + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#observeLogs(); - }); + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#observeLogs(); + } + private get _logViewerContext() { + return this.#logViewerContext; } #observeLogs() { - if (!this.#logViewerContext) return; - - this.observe(this.#logViewerContext.logs, (logs) => { + this.observe(this._logViewerContext?.logs, (logs) => { this._logs = logs ?? []; }); - this.observe(this.#logViewerContext.isLoadingLogs, (isLoading) => { - this._isLoading = isLoading === null ? this._isLoading : isLoading; + this.observe(this._logViewerContext?.isLoadingLogs, (isLoading) => { + this._isLoading = isLoading ?? this._isLoading; }); - this.observe(this.#logViewerContext.logsTotal, (total) => { + this.observe(this._logViewerContext?.logsTotal, (total) => { this._logsTotal = total ?? 0; }); - this.observe(this.#logViewerContext.sortingDirection, (direction) => { - this._sortingDirection = direction; + this.observe(this._logViewerContext?.sortingDirection, (direction) => { + this._sortingDirection = direction ?? this._sortingDirection; }); } #sortLogs() { - this.#logViewerContext?.toggleSortOrder(); - this.#logViewerContext?.setCurrentPage(1); - this.#logViewerContext?.getLogs(); + this._logViewerContext?.toggleSortOrder(); + this._logViewerContext?.setCurrentPage(1); + this._logViewerContext?.getLogs(); } #onPageChange(event: Event): void { const current = (event.target as UUIPaginationElement).current; - this.#logViewerContext?.setCurrentPage(current); - this.#logViewerContext?.getLogs(); + this._logViewerContext?.setCurrentPage(current); + this._logViewerContext?.getLogs(); this._logsScrollContainer.scrollTop = 0; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-polling-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-polling-button.element.ts index 5bef958d4504..6955c832285e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-polling-button.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-polling-button.element.ts @@ -1,12 +1,9 @@ -import type { - UmbPoolingConfig, - UmbPoolingInterval, - UmbLogViewerWorkspaceContext, -} from '../../../logviewer-workspace.context.js'; +import type { UmbPoolingConfig, UmbPoolingInterval } from '../../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-polling-button') export class UmbLogViewerPollingButtonElement extends UmbLitElement { @@ -18,30 +15,29 @@ export class UmbLogViewerPollingButtonElement extends UmbLitElement { #pollingIntervals: UmbPoolingInterval[] = [2000, 5000, 10000, 20000, 30000]; - #logViewerContext?: UmbLogViewerWorkspaceContext; + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#observePoolingConfig(); - }); + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#observePoolingConfig(); + } + private get _logViewerContext() { + return this.#logViewerContext; } #observePoolingConfig() { - if (!this.#logViewerContext) return; - - this.observe(this.#logViewerContext.polling, (poolingConfig) => { - this._poolingConfig = { ...poolingConfig }; + this.observe(this._logViewerContext?.polling, (poolingConfig) => { + this._poolingConfig = poolingConfig ? { ...poolingConfig } : { enabled: false, interval: 0 }; }); } #togglePolling() { - this.#logViewerContext?.togglePolling(); + this._logViewerContext?.togglePolling(); } #setPolingInterval = (interval: UmbPoolingInterval) => { - this.#logViewerContext?.setPollingInterval(interval); + this._logViewerContext?.setPollingInterval(interval); this.#closePoolingPopover(); }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts index 55ad9b74e79c..e4563d976123 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts @@ -1,4 +1,3 @@ -import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js'; import { UMB_LOG_VIEWER_SAVE_SEARCH_MODAL } from './log-viewer-search-input-modal.modal-token.js'; import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit'; @@ -12,6 +11,7 @@ import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import './log-viewer-search-input-modal.element.js'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-search-input') export class UmbLogViewerSearchInputElement extends UmbLitElement { @@ -33,15 +33,20 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { // TODO: Revisit this code, to not use RxJS directly: #inputQuery$ = new Subject(); - #logViewerContext?: UmbLogViewerWorkspaceContext; + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; + + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#observeStuff(); + this.#logViewerContext?.getSavedSearches(); + } + private get _logViewerContext() { + return this.#logViewerContext; + } constructor() { super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#observeStuff(); - this.#logViewerContext?.getSavedSearches(); - }); this.#inputQuery$ .pipe( @@ -49,7 +54,7 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { debounceTime(250), ) .subscribe((query) => { - this.#logViewerContext?.setFilterExpression(query); + this._logViewerContext?.setFilterExpression(query); this.#persist(query); this._isQuerySaved = this._savedSearches.some((search) => search.query === query); this._showLoader = false; @@ -57,15 +62,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { } #observeStuff() { - if (!this.#logViewerContext) return; - this.observe(this.#logViewerContext.savedSearches, (savedSearches) => { + this.observe(this._logViewerContext?.savedSearches, (savedSearches) => { this._savedSearches = savedSearches?.items ?? []; this._isQuerySaved = this._savedSearches.some((search) => search.query === this._inputQuery); }); - this.observe(this.#logViewerContext.filterExpression, (query) => { - this._inputQuery = query; - this._isQuerySaved = this._savedSearches.some((search) => search.query === query); + this.observe(this._logViewerContext?.filterExpression, (query) => { + this._inputQuery = query ?? ''; + this._isQuerySaved = this._savedSearches.some((search) => search.query === this._inputQuery); }); } @@ -92,11 +96,11 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { #clearQuery() { this.#inputQuery$.next(''); - this.#logViewerContext?.setFilterExpression(''); + this._logViewerContext?.setFilterExpression(''); } #saveSearch(savedSearch: SavedLogSearchResponseModel) { - this.#logViewerContext?.saveSearch(savedSearch); + this._logViewerContext?.saveSearch(savedSearch); } async #removeSearch(name: string) { @@ -107,7 +111,7 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { confirmLabel: 'Delete', }); - this.#logViewerContext?.removeSearch({ name }); + this._logViewerContext?.removeSearch({ name }); //this.dispatchEvent(new UmbDeleteEvent()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/log-search-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/log-search-view.element.ts index b7e9e1d1c123..317711c2e51a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/log-search-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/log-search-view.element.ts @@ -1,32 +1,32 @@ -import type { UmbLogViewerWorkspaceContext } from '../../logviewer-workspace.context.js'; import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../logviewer-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { consumeContext } from '@umbraco-cms/backoffice/context-api'; @customElement('umb-log-viewer-search-view') export class UmbLogViewerSearchViewElement extends UmbLitElement { @state() private _canShowLogs = true; - #logViewerContext?: UmbLogViewerWorkspaceContext; + #logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE; - #canShowLogsObserver?: UmbObserverController; - - constructor() { - super(); - this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => { - this.#logViewerContext = instance; - this.#observeCanShowLogs(); - }); + @consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT }) + private set _logViewerContext(value) { + this.#logViewerContext = value; + this.#observeCanShowLogs(); } + private get _logViewerContext() { + return this.#logViewerContext; + } + + #canShowLogsObserver?: UmbObserverController; #observeCanShowLogs() { if (this.#canShowLogsObserver) this.#canShowLogsObserver.destroy(); - if (!this.#logViewerContext) return; - this.#canShowLogsObserver = this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => { + this.#canShowLogsObserver = this.observe(this._logViewerContext?.canShowLogs, (canShowLogs) => { this._canShowLogs = canShowLogs ?? this._canShowLogs; }); } From 62edad17a1f6c2c170395431a93901bde492ea0b Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 16 Oct 2025 20:50:59 +0200 Subject: [PATCH 041/155] Bumped version to 16.3.1. --- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index de785cc2058b..766ffa96baea 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.3.0", + "version": "16.3.1", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index e71c12fcd95b..922b55eca9ce 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": "16.3.0", + "version": "16.3.1", "assemblyVersion": { "precision": "build" }, From 31bcbc1147f66cab1837a9afe4732ad0bb35b959 Mon Sep 17 00:00:00 2001 From: Ben White Date: Thu, 16 Oct 2025 20:06:10 +0100 Subject: [PATCH 042/155] Don't use non-generic ILogger as a fallback in BlockEditorPropertyValueEditor (#20532) Update logger service retrieval in BlockEditorPropertyValueEditor --- .../PropertyEditors/BlockEditorPropertyValueEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 10c752088bec..84299d63992a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -51,7 +51,7 @@ protected BlockEditorPropertyValueEditor( languageService, ioHelper, attribute, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService>>()) { } From 6458bb40f9eaf3a88318cc38268f3e522acf8f0c Mon Sep 17 00:00:00 2001 From: Ben White Date: Thu, 16 Oct 2025 20:06:10 +0100 Subject: [PATCH 043/155] Don't use non-generic ILogger as a fallback in BlockEditorPropertyValueEditor (#20532) Update logger service retrieval in BlockEditorPropertyValueEditor --- .../PropertyEditors/BlockEditorPropertyValueEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 10c752088bec..84299d63992a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -51,7 +51,7 @@ protected BlockEditorPropertyValueEditor( languageService, ioHelper, attribute, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService>>()) { } From d17ba805b2f42a68dded771fab8fbcd558737ad4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:09:15 +0200 Subject: [PATCH 044/155] build(deps): bumps @umbraco-ui/uui from 1.16.0-rc.0 to 1.16.0 --- src/Umbraco.Web.UI.Client/package-lock.json | 970 +++++++++--------- .../src/external/uui/package.json | 4 +- 2 files changed, 487 insertions(+), 487 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index c9c1c2115b6c..b2657a2a273b 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -3753,909 +3753,909 @@ "link": true }, "node_modules/@umbraco-ui/uui": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.16.0-rc.0.tgz", - "integrity": "sha512-iTz/KfvxkO/gxxz9x19glpVTIZ4mZXRU3/EM2oENXCJw8sbmMkGecwO2PLFLSbq8YXmOmq5b5dUSVsTRpa91iQ==", - "license": "MIT", - "dependencies": { - "@umbraco-ui/uui-action-bar": "1.16.0-rc.0", - "@umbraco-ui/uui-avatar": "1.16.0-rc.0", - "@umbraco-ui/uui-avatar-group": "1.16.0-rc.0", - "@umbraco-ui/uui-badge": "1.16.0-rc.0", - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-boolean-input": "1.16.0-rc.0", - "@umbraco-ui/uui-box": "1.16.0-rc.0", - "@umbraco-ui/uui-breadcrumbs": "1.16.0-rc.0", - "@umbraco-ui/uui-button": "1.16.0-rc.0", - "@umbraco-ui/uui-button-copy-text": "1.16.0-rc.0", - "@umbraco-ui/uui-button-group": "1.16.0-rc.0", - "@umbraco-ui/uui-button-inline-create": "1.16.0-rc.0", - "@umbraco-ui/uui-card": "1.16.0-rc.0", - "@umbraco-ui/uui-card-block-type": "1.16.0-rc.0", - "@umbraco-ui/uui-card-content-node": "1.16.0-rc.0", - "@umbraco-ui/uui-card-media": "1.16.0-rc.0", - "@umbraco-ui/uui-card-user": "1.16.0-rc.0", - "@umbraco-ui/uui-caret": "1.16.0-rc.0", - "@umbraco-ui/uui-checkbox": "1.16.0-rc.0", - "@umbraco-ui/uui-color-area": "1.16.0-rc.0", - "@umbraco-ui/uui-color-picker": "1.16.0-rc.0", - "@umbraco-ui/uui-color-slider": "1.16.0-rc.0", - "@umbraco-ui/uui-color-swatch": "1.16.0-rc.0", - "@umbraco-ui/uui-color-swatches": "1.16.0-rc.0", - "@umbraco-ui/uui-combobox": "1.16.0-rc.0", - "@umbraco-ui/uui-combobox-list": "1.16.0-rc.0", - "@umbraco-ui/uui-css": "1.16.0-rc.0", - "@umbraco-ui/uui-dialog": "1.16.0-rc.0", - "@umbraco-ui/uui-dialog-layout": "1.16.0-rc.0", - "@umbraco-ui/uui-file-dropzone": "1.16.0-rc.0", - "@umbraco-ui/uui-file-preview": "1.16.0-rc.0", - "@umbraco-ui/uui-form": "1.16.0-rc.0", - "@umbraco-ui/uui-form-layout-item": "1.16.0-rc.0", - "@umbraco-ui/uui-form-validation-message": "1.16.0-rc.0", - "@umbraco-ui/uui-icon": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.16.0-rc.0", - "@umbraco-ui/uui-input": "1.16.0-rc.0", - "@umbraco-ui/uui-input-file": "1.16.0-rc.0", - "@umbraco-ui/uui-input-lock": "1.16.0-rc.0", - "@umbraco-ui/uui-input-password": "1.16.0-rc.0", - "@umbraco-ui/uui-keyboard-shortcut": "1.16.0-rc.0", - "@umbraco-ui/uui-label": "1.16.0-rc.0", - "@umbraco-ui/uui-loader": "1.16.0-rc.0", - "@umbraco-ui/uui-loader-bar": "1.16.0-rc.0", - "@umbraco-ui/uui-loader-circle": "1.16.0-rc.0", - "@umbraco-ui/uui-menu-item": "1.16.0-rc.0", - "@umbraco-ui/uui-modal": "1.16.0-rc.0", - "@umbraco-ui/uui-pagination": "1.16.0-rc.0", - "@umbraco-ui/uui-popover": "1.16.0-rc.0", - "@umbraco-ui/uui-popover-container": "1.16.0-rc.0", - "@umbraco-ui/uui-progress-bar": "1.16.0-rc.0", - "@umbraco-ui/uui-radio": "1.16.0-rc.0", - "@umbraco-ui/uui-range-slider": "1.16.0-rc.0", - "@umbraco-ui/uui-ref": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-list": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node-data-type": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node-document-type": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node-form": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node-member": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node-package": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node-user": "1.16.0-rc.0", - "@umbraco-ui/uui-scroll-container": "1.16.0-rc.0", - "@umbraco-ui/uui-select": "1.16.0-rc.0", - "@umbraco-ui/uui-slider": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-expand": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-file": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-file-dropzone": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-file-thumbnail": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-folder": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-lock": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-more": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-sort": "1.16.0-rc.0", - "@umbraco-ui/uui-table": "1.16.0-rc.0", - "@umbraco-ui/uui-tabs": "1.16.0-rc.0", - "@umbraco-ui/uui-tag": "1.16.0-rc.0", - "@umbraco-ui/uui-textarea": "1.16.0-rc.0", - "@umbraco-ui/uui-toast-notification": "1.16.0-rc.0", - "@umbraco-ui/uui-toast-notification-container": "1.16.0-rc.0", - "@umbraco-ui/uui-toast-notification-layout": "1.16.0-rc.0", - "@umbraco-ui/uui-toggle": "1.16.0-rc.0", - "@umbraco-ui/uui-visually-hidden": "1.16.0-rc.0" + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.16.0.tgz", + "integrity": "sha512-aWHFSTf+FkPiMirT25UjmUD7wcyQqxvO7btO3AeA7Ogx7R3KiVNulHpPNPgTsyaHFWRcVmxhWDHaib4GHoOJXQ==", + "license": "MIT", + "dependencies": { + "@umbraco-ui/uui-action-bar": "1.16.0", + "@umbraco-ui/uui-avatar": "1.16.0", + "@umbraco-ui/uui-avatar-group": "1.16.0", + "@umbraco-ui/uui-badge": "1.16.0", + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-boolean-input": "1.16.0", + "@umbraco-ui/uui-box": "1.16.0", + "@umbraco-ui/uui-breadcrumbs": "1.16.0", + "@umbraco-ui/uui-button": "1.16.0", + "@umbraco-ui/uui-button-copy-text": "1.16.0", + "@umbraco-ui/uui-button-group": "1.16.0", + "@umbraco-ui/uui-button-inline-create": "1.16.0", + "@umbraco-ui/uui-card": "1.16.0", + "@umbraco-ui/uui-card-block-type": "1.16.0", + "@umbraco-ui/uui-card-content-node": "1.16.0", + "@umbraco-ui/uui-card-media": "1.16.0", + "@umbraco-ui/uui-card-user": "1.16.0", + "@umbraco-ui/uui-caret": "1.16.0", + "@umbraco-ui/uui-checkbox": "1.16.0", + "@umbraco-ui/uui-color-area": "1.16.0", + "@umbraco-ui/uui-color-picker": "1.16.0", + "@umbraco-ui/uui-color-slider": "1.16.0", + "@umbraco-ui/uui-color-swatch": "1.16.0", + "@umbraco-ui/uui-color-swatches": "1.16.0", + "@umbraco-ui/uui-combobox": "1.16.0", + "@umbraco-ui/uui-combobox-list": "1.16.0", + "@umbraco-ui/uui-css": "1.16.0", + "@umbraco-ui/uui-dialog": "1.16.0", + "@umbraco-ui/uui-dialog-layout": "1.16.0", + "@umbraco-ui/uui-file-dropzone": "1.16.0", + "@umbraco-ui/uui-file-preview": "1.16.0", + "@umbraco-ui/uui-form": "1.16.0", + "@umbraco-ui/uui-form-layout-item": "1.16.0", + "@umbraco-ui/uui-form-validation-message": "1.16.0", + "@umbraco-ui/uui-icon": "1.16.0", + "@umbraco-ui/uui-icon-registry": "1.16.0", + "@umbraco-ui/uui-icon-registry-essential": "1.16.0", + "@umbraco-ui/uui-input": "1.16.0", + "@umbraco-ui/uui-input-file": "1.16.0", + "@umbraco-ui/uui-input-lock": "1.16.0", + "@umbraco-ui/uui-input-password": "1.16.0", + "@umbraco-ui/uui-keyboard-shortcut": "1.16.0", + "@umbraco-ui/uui-label": "1.16.0", + "@umbraco-ui/uui-loader": "1.16.0", + "@umbraco-ui/uui-loader-bar": "1.16.0", + "@umbraco-ui/uui-loader-circle": "1.16.0", + "@umbraco-ui/uui-menu-item": "1.16.0", + "@umbraco-ui/uui-modal": "1.16.0", + "@umbraco-ui/uui-pagination": "1.16.0", + "@umbraco-ui/uui-popover": "1.16.0", + "@umbraco-ui/uui-popover-container": "1.16.0", + "@umbraco-ui/uui-progress-bar": "1.16.0", + "@umbraco-ui/uui-radio": "1.16.0", + "@umbraco-ui/uui-range-slider": "1.16.0", + "@umbraco-ui/uui-ref": "1.16.0", + "@umbraco-ui/uui-ref-list": "1.16.0", + "@umbraco-ui/uui-ref-node": "1.16.0", + "@umbraco-ui/uui-ref-node-data-type": "1.16.0", + "@umbraco-ui/uui-ref-node-document-type": "1.16.0", + "@umbraco-ui/uui-ref-node-form": "1.16.0", + "@umbraco-ui/uui-ref-node-member": "1.16.0", + "@umbraco-ui/uui-ref-node-package": "1.16.0", + "@umbraco-ui/uui-ref-node-user": "1.16.0", + "@umbraco-ui/uui-scroll-container": "1.16.0", + "@umbraco-ui/uui-select": "1.16.0", + "@umbraco-ui/uui-slider": "1.16.0", + "@umbraco-ui/uui-symbol-expand": "1.16.0", + "@umbraco-ui/uui-symbol-file": "1.16.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.16.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.16.0", + "@umbraco-ui/uui-symbol-folder": "1.16.0", + "@umbraco-ui/uui-symbol-lock": "1.16.0", + "@umbraco-ui/uui-symbol-more": "1.16.0", + "@umbraco-ui/uui-symbol-sort": "1.16.0", + "@umbraco-ui/uui-table": "1.16.0", + "@umbraco-ui/uui-tabs": "1.16.0", + "@umbraco-ui/uui-tag": "1.16.0", + "@umbraco-ui/uui-textarea": "1.16.0", + "@umbraco-ui/uui-toast-notification": "1.16.0", + "@umbraco-ui/uui-toast-notification-container": "1.16.0", + "@umbraco-ui/uui-toast-notification-layout": "1.16.0", + "@umbraco-ui/uui-toggle": "1.16.0", + "@umbraco-ui/uui-visually-hidden": "1.16.0" } }, "node_modules/@umbraco-ui/uui-action-bar": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.16.0-rc.0.tgz", - "integrity": "sha512-7uGyzPQIaW5PKwXhsHY5ZfdOB3D254YOEyazh/ls7J3GcxIKiqzSflYn/d8BXrjWP2qsDovAcbghS7oDxZf3rw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.16.0.tgz", + "integrity": "sha512-WM08j2cGcJcbXWS6Pb9FdhaKDz3+EUSuoxrsZoGkJBJMriZLv4gq9EcE5RIstUbT8JmDPQ7uT3SDT2gZWl07MQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-button-group": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-button-group": "1.16.0" } }, "node_modules/@umbraco-ui/uui-avatar": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.16.0-rc.0.tgz", - "integrity": "sha512-C51NWbKtNTvurRYU9Ni4GCk2CL+yM4+cMNP3sPlLJCj3Epu6nOiYD0txSguTCj1TcBxmQLW5lkuEV3+5IqxZAQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.16.0.tgz", + "integrity": "sha512-1u6+hOLy5NrFh5/Z4Kp88y3Mhq+FYCZRwPb+5lSutm+aMy27dehRKkZqlbptWn/qocUCibDxQpruvu/UMtVQtg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-avatar-group": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.16.0-rc.0.tgz", - "integrity": "sha512-C0/5gHU6lIZPrgvizZicsI5Z8P7f8Kulq9buzc7E5c6r/vgCTl5JTR8cjcl5P5KXDEg/RgPgrcNyGep+Qt8cGQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.16.0.tgz", + "integrity": "sha512-509UZzUSD/JhJEVLEpT5ltccHpEw8RxoZbG+hJeg23Oh3jNuRrKvuiyOut5c6JfjMdawHw6vPivVwjqCmbZG5g==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-avatar": "1.16.0-rc.0", - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-avatar": "1.16.0", + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-badge": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.16.0-rc.0.tgz", - "integrity": "sha512-ChOkIJWoecMpdBTNj4q0l4WkhzMW5qTX5UFy2500veE7HNKv8yIZHYgyFlNNrl10oAsASeSx1uP/kVv1Jxmy9A==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.16.0.tgz", + "integrity": "sha512-sHo71JOxxk0EufgYfCl9miuYgM1LDSnmtHedvDGs776htMFkLo3W/cFWgIXabAHZeSj4R5UWMGDNsugwv03R+w==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-base": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.16.0-rc.0.tgz", - "integrity": "sha512-SjW0yfUhGy17dbpB6gYujR70bbQPW5aGjGTzI8XcIZ2A+GcCuvWjfAR9WUfFzqJk3dQn0SZjaCOIzN+5qazysw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.16.0.tgz", + "integrity": "sha512-8i9bdcSrdR/4lWm0xetr3R3w3Rod3YVbIddHqbb3iVrr0TmPDTVA48tnOsJyQFAvTrh2LZjiETvEve7pBy4WQA==", "license": "MIT", "peerDependencies": { "lit": ">=2.8.0" } }, "node_modules/@umbraco-ui/uui-boolean-input": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.16.0-rc.0.tgz", - "integrity": "sha512-MHJS6MxRn35H9PJk1PKnCQZFln/2m+T07F4Kfb93KT4cARgTgYgntI8PDi1xK5YdlW77mGunNdEYgFmUBNOLlQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.16.0.tgz", + "integrity": "sha512-IRU2z3GV+WzyjUvIMeErYeOE/0GyOpItsXxfmxsEENT/7qq4UMk28fIxY9IdDfI285WP0N3kezWkPBPlCKBcNQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-box": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.16.0-rc.0.tgz", - "integrity": "sha512-Cuvlccf8LTgBVDiXdA3Ba8hvHJb3np3YyFyqFmbTdyGgpQ2/tBtbhgcA7LD2mvOK+I2vFybyHSPzI8gWBR60AA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.16.0.tgz", + "integrity": "sha512-/Wgnv2jr6wKG436WNjBdGq6x+aExiZhZgLPnzrTcaevy85MM5pJZWgY1+aI+pJclgU6WtRMii2+C8MZL2Qmh0w==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-css": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-css": "1.16.0" } }, "node_modules/@umbraco-ui/uui-breadcrumbs": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.16.0-rc.0.tgz", - "integrity": "sha512-7/UFyLRR+m4MeuOscclhr/8zzbLGl1D1+uYaIfK6rysP0MkAOOJf7yVjKbEzml58P1XYIMnnT1lcJE0DoyfKkg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.16.0.tgz", + "integrity": "sha512-PuLcxG+3ZeSXKH3M0Kkh3eVYOEJPwLfg+6+b4UXxV/O9p0tUFbNPc8ciggL/1ZBXYXjsQnFTaOQWV4zGpnCnFQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-button": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.16.0-rc.0.tgz", - "integrity": "sha512-02a3PDDJCcRdTkhLyvmrK3WUGZA25rZ6QQChBhUf1p2DsJU01k+vc54VJ7C984IMFiPuvMEkxig9Ks8aZhxcxg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.16.0.tgz", + "integrity": "sha512-0nTAx/GVOdGvlekkIxZp1nJs2E1DRzbdUnARl6RN5Oc40HowW9oO5oJvDIpoZcsWqkqWzFTQqVgE1z1PafKHZw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-icon-registry-essential": "1.16.0" } }, "node_modules/@umbraco-ui/uui-button-copy-text": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-copy-text/-/uui-button-copy-text-1.16.0-rc.0.tgz", - "integrity": "sha512-Chk3qIVQ11OEx0UscNmvhqSwW+M1NDikHt34ynriJKsXhONFvjSE12K06zSNhB2kbbkS+aVmJ/PzalkExMfjIw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-copy-text/-/uui-button-copy-text-1.16.0.tgz", + "integrity": "sha512-CXjJzLbedqHtlza2zspSWNZCw5XhHV5QkPFzRI5Zd8FwFZop1/UgM2GQeSrMaWdfpznbWvfUqnvSYt9wYEubVg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-button": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-button": "1.16.0" } }, "node_modules/@umbraco-ui/uui-button-group": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.16.0-rc.0.tgz", - "integrity": "sha512-vUwWO82yvuj2Db5bp7oFC/EV3jQpkxQPhR460AGyh68UlS2qTgUR5rsB+AOfUsH/UIbHjrhh+Wn6DcF2hEZzTg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.16.0.tgz", + "integrity": "sha512-ygici33P70SJqa2SSjdSVd8paSKqHwewKJMcyIF/IehDepnDP0ngSHWA23B/sEzJNJgq0Zngo9g3jlhZz6H6GA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-button-inline-create": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.16.0-rc.0.tgz", - "integrity": "sha512-tGLxtmafqWwQoYgbm3QdbRri0iq7gv+89n9LwTK/NcFq3bDbqcKV0xw4LV/XC6kweAoOjtFAO3hzOuow/+/vnw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.16.0.tgz", + "integrity": "sha512-To9K/mYXLm4SGih3uA8/jbZd/ewWKVvYH6b26F5fvEDVT+X9fjJchKT7J/u0a4C7wghvVNT+os7H0rxS3yTXiQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-card": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.16.0-rc.0.tgz", - "integrity": "sha512-bAEAgemTDebXF8VX5KO9Oh/i7n5ZyuGb+EzRCDGpKlw33to0jcyqRFTdS5XRdPcoD6T8awO87145KBTxCMk10Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.16.0.tgz", + "integrity": "sha512-o/8vDLT03WnQsJKyD8r7PzxvhD3loRI7pL3tZU1BeSDcFAOZPPWIudQ/OwYeJnMI1iHkd2eTu0h22B/sXOfIIQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-checkbox": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-checkbox": "1.16.0" } }, "node_modules/@umbraco-ui/uui-card-block-type": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-block-type/-/uui-card-block-type-1.16.0-rc.0.tgz", - "integrity": "sha512-jRV4BQpG/BgIX/yctW+eZHJ1Ncdu3H7gNTLsh40UOlHHR0T0aAkUFp2jub74Wq8yZjeTr2cT5/eQy4aHnusNkg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-block-type/-/uui-card-block-type-1.16.0.tgz", + "integrity": "sha512-Xpq/kB/ofSn067teaOyS4hEsEt/WUlrJ0opTFgkwHxsWg9rvMzUtg2nc2JGMoIqJ64/40Axcx0jmmchIDUcbsQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-card": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-card": "1.16.0" } }, "node_modules/@umbraco-ui/uui-card-content-node": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.16.0-rc.0.tgz", - "integrity": "sha512-oWLiU6IjibpCU+ANUuSLZ81cA1QFKRs5qJO5kDmzlW8EFfec3xYQefnmU+mQ9M9nueXMhFPBS4cNXaTvW/oyWA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.16.0.tgz", + "integrity": "sha512-VPRDFrZSPLDGE3kAarW78dZHIFBhwXakyj7PM278tcXGdfSM7M9HsLXME6DhlleOYfSV07wHXm0UXKieqO7vgw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-card": "1.16.0-rc.0", - "@umbraco-ui/uui-icon": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-card": "1.16.0", + "@umbraco-ui/uui-icon": "1.16.0" } }, "node_modules/@umbraco-ui/uui-card-media": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.16.0-rc.0.tgz", - "integrity": "sha512-ZXDsQ6ciuf9ordiVG9DtSxO5jhMejG3PZALf1e+v17YOEacCWoKooTcKGV1PHI+59lljbLvzgDHp883zX1difQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.16.0.tgz", + "integrity": "sha512-IHFCnXr4Bdpj/aUn+jpmlYx9L0FzeWTwt+cb29b4oP0cjIiVaJIrkOCSIl3SF8ncrKfMlTjlgBe0t0sP4mjeug==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-card": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-file": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-folder": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-card": "1.16.0", + "@umbraco-ui/uui-symbol-file": "1.16.0", + "@umbraco-ui/uui-symbol-folder": "1.16.0" } }, "node_modules/@umbraco-ui/uui-card-user": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.16.0-rc.0.tgz", - "integrity": "sha512-PgHiWqe430Q/RS7nq2iJ35iVNMwsIhx3BnI7KOQQ8YwyLCpLef/TYV9tzGkTw9De27G1yKUaXn5/Wu+C8tccZw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.16.0.tgz", + "integrity": "sha512-Ne64+ssQrpP9zJvlJhH1Y5xlEDMW1lG17Orj6XH99iDtGdrnug9FjRE4vpNfAVRIb9P1pf7xNJtq2XqCJHvqOQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-avatar": "1.16.0-rc.0", - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-card": "1.16.0-rc.0" + "@umbraco-ui/uui-avatar": "1.16.0", + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-card": "1.16.0" } }, "node_modules/@umbraco-ui/uui-caret": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.16.0-rc.0.tgz", - "integrity": "sha512-sP64XZBB+60pyMdIKPERF02PrRYeOZqC/l6P5h2KnygSqUCmHQwTg2KcsoJ61UW5l9PjIdI5YlzCrHStuwZzaA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.16.0.tgz", + "integrity": "sha512-B3xNrwkQBwye9ydlrvnYfbJyiLqwQEbpldfaJnjLvlW9xVhOFps2NfeRyXcdsvruaIwjml7aB18GVYDCd/PSlw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-checkbox": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.16.0-rc.0.tgz", - "integrity": "sha512-KussDn46ZlBKTYBT+eiEK7rV4KEMmsDt5oi/LY+ZxqwKt8Ctn8Ubc/zB8dM08ihGFACAVmS+R2X3vmeHG8rYLw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.16.0.tgz", + "integrity": "sha512-4z8XrZ0InVArdHKO7L7uwAMwUwHyQKqSYShE74VHHWOibySciJ/zPx3hFO3eQ7EBL3Kj+4raun5Ah5jHUlDZwA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-boolean-input": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-boolean-input": "1.16.0", + "@umbraco-ui/uui-icon-registry-essential": "1.16.0" } }, "node_modules/@umbraco-ui/uui-color-area": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.16.0-rc.0.tgz", - "integrity": "sha512-vKj1faPcp1yl2YWKzrkBC0s+gexzjBE5WxTVMS7n7rnvz9Ngxd0DvQADsiTS/Q2/WrEnORG7nXC3SlFvp/bigA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.16.0.tgz", + "integrity": "sha512-wiK9WNZWZ5yFd3ouTZOcoUSm+2iNZIFlGTaTScnG/DiLCBs6DUvdbSbVHueY1cGWbOx/R8N01kZBls1fk8kaHw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", + "@umbraco-ui/uui-base": "1.16.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-picker": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.16.0-rc.0.tgz", - "integrity": "sha512-54EDVEXmk2cWRf6DvogFxEwtzbo4e9TdwY+s+L7DSA2VkKNM25tml7hC/ItHGVN+qrmz01KIVEJFtOwdXt+krw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.16.0.tgz", + "integrity": "sha512-IilZw7Qn+2QF80OXktnoY1RI45ggl8o+QyF5a6zjd2gl5BfwAVx/uFCnpDfjH6LKtRw9WvuPKHQyM0/mfi5I4g==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-popover-container": "1.16.0-rc.0", + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-popover-container": "1.16.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-slider": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.16.0-rc.0.tgz", - "integrity": "sha512-47ixTPpZIcpqNSzkSKXymuZDYyMQXRJkYFNi2B78bIOmekH+aab5TkQOYdhQ7O1c8+O0TxDWsCQBBgrHHVSMOQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.16.0.tgz", + "integrity": "sha512-GDlAv+75efrOq9K/mZSKLwmc/ZG82hCaRMpWI4guKKvJhcukIcg7Bt/jQrDrtEGKCYvMJpNzbqZ41b+x23EQEg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-color-swatch": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.16.0-rc.0.tgz", - "integrity": "sha512-iub68xyjyf6fyBcG8HvTbp00NjTQDr1JadeYRCrCxepNfSQN531MFTVZnMz0OdI8qH69tKYQFyokghZ00iPJ6w==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.16.0.tgz", + "integrity": "sha512-I+0iEkIGXzoDfLUj0duUJsdf71FC1EBqNzAH/X5noiWc+RZiAAw5EvXm7rZO69oDNOQMwt/yMCBLJQp2kYOQTA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.16.0-rc.0", + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-icon-registry-essential": "1.16.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-swatches": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.16.0-rc.0.tgz", - "integrity": "sha512-ypN5CNzngFvpqCXrkiNHCrQS7XEqhMyaKmDSUc065uVs7Py0FFhXpsr8HQGWuNx5ER4oakdxiAtp8MEMCggbyQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.16.0.tgz", + "integrity": "sha512-i58T2PRYzViBTo7OtJAGi5inVF8jxVYBmLL7nb3dpNjUFTZZufRKTr3AsVS7+pCGEogFmyNbcNztmmEMdU4ekA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-color-swatch": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-color-swatch": "1.16.0" } }, "node_modules/@umbraco-ui/uui-combobox": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.16.0-rc.0.tgz", - "integrity": "sha512-gk89KBira+Yh29KUS5nb6yaQILdI6YF/TMpXtDL6OlUlaIeaSWpCmIKtX/MzqZH4Ov/fspRKxjRFnYVPxU8Qbg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.16.0.tgz", + "integrity": "sha512-zjeNG+7r5J4UgdeWh8Osktkjk/Uret5tu8mUtpp0Z6LIbxISUKEt9QlbjPPorxB3V0ENKUJ2c5KZZtpj7mLihQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-button": "1.16.0-rc.0", - "@umbraco-ui/uui-combobox-list": "1.16.0-rc.0", - "@umbraco-ui/uui-icon": "1.16.0-rc.0", - "@umbraco-ui/uui-popover-container": "1.16.0-rc.0", - "@umbraco-ui/uui-scroll-container": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-expand": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-button": "1.16.0", + "@umbraco-ui/uui-combobox-list": "1.16.0", + "@umbraco-ui/uui-icon": "1.16.0", + "@umbraco-ui/uui-popover-container": "1.16.0", + "@umbraco-ui/uui-scroll-container": "1.16.0", + "@umbraco-ui/uui-symbol-expand": "1.16.0" } }, "node_modules/@umbraco-ui/uui-combobox-list": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.16.0-rc.0.tgz", - "integrity": "sha512-j1y3NxbH+emmpIEr3eSWAcYQDB/QjFThfZANoOPI88VeCPCT12szoeWNC18ymEed3HblC+dxZ9XQ+ILApVlyFw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.16.0.tgz", + "integrity": "sha512-gNFheYUtzMvQudvzoRhDgJk9zziFTxSyu92aYzyoyhh7M098gJfqU+fo7Teqqiuyb0NEiZPThcNrUT9MD2LD3A==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-css": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.16.0-rc.0.tgz", - "integrity": "sha512-jQgQfo2+bdTzj6BbW3ETL8YDrnBQzvoAVp4G7P/XApupgo9DLdhWgU13jiJ+j9fr2LzegTRYN1U1rQ8rXrnUfw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.16.0.tgz", + "integrity": "sha512-uyr5zWOfqSH2z1He+i8vZVYZk8Bq4iKMXqCerKHuiNoCZOaW9Kg8n+mJXhQ3Kz5+r9RXUbJThMJO/6/8NFYvbQ==", "license": "MIT", "peerDependencies": { "lit": ">=2.8.0" } }, "node_modules/@umbraco-ui/uui-dialog": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.16.0-rc.0.tgz", - "integrity": "sha512-t0dOuBlBXuzFr2B4U9qPaH38FFFgKDt+rUiNmKInCcE1uiPq7e7hcrpOlTg2iVMjTzJGXWhUgdykNXOXKoQAKA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.16.0.tgz", + "integrity": "sha512-dq+daSQKAIdsP+2QhM6HmU9Nr5VVzbxwQEYLVvAcmYcw4K98TVpP6AyHu5dPDP9vl4EBBXUrrZuXFjU+Mh8/xQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-css": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-css": "1.16.0" } }, "node_modules/@umbraco-ui/uui-dialog-layout": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.16.0-rc.0.tgz", - "integrity": "sha512-3HN3brJW28MWRn/klpvwgWPSrZzgoyouv2SiLSaTeLJM7bGfB8iVOU7WkkgVBujKR7wzXsWmzBuuSNAVLoX4XA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.16.0.tgz", + "integrity": "sha512-iRpmlzp1PAUpF6Ol2EWubdABIgpJE6QmBzaQONm3Mmwe1wLxMGp5+o33wHU9WSTh8kDrH/U5mWtua6Xtyf5JFA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-file-dropzone": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.16.0-rc.0.tgz", - "integrity": "sha512-iUe8NFW9V/2Vj4ysdWlwKpHvPegFqCkggsyT+TmH2iG4BA4h5GMDRwO6j6ooOKJAuZqzg33YRAwL74Mts43KgA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.16.0.tgz", + "integrity": "sha512-B3Zy6jlyK68ntaC4idv7fzd9NVyc4VVjn68DgkvnHR76Mp8zmOgT0g7K7/WM33IPw/n/ZfBhM1KEb+ry3i9/bg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-file-dropzone": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.16.0" } }, "node_modules/@umbraco-ui/uui-file-preview": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.16.0-rc.0.tgz", - "integrity": "sha512-jEH0ZaRvexLCQwvUxa9+JaIM7aqV4FWaAMLOQNKE3aJuFJhOYmCkR6txU/Ed7IMEavvSaQpMspBqQTXq8gom1g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.16.0.tgz", + "integrity": "sha512-A+jych/xEUOssZjqWtW04nD1GcVOHnonTlPdrDaFh9PhwQAL0PREBbHZnkLJBS4z+HKWhsXOUeQ9ju0YAtbRuQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-file": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-file-thumbnail": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-folder": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-symbol-file": "1.16.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.16.0", + "@umbraco-ui/uui-symbol-folder": "1.16.0" } }, "node_modules/@umbraco-ui/uui-form": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.16.0-rc.0.tgz", - "integrity": "sha512-352RWarESuFiFj76LJ8mUEd849ErexOSdm8zEwpsHYlZxbBlICiTRkWQrykPb50SwOoZpHeBp0ZH/dqu2vRA+A==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.16.0.tgz", + "integrity": "sha512-mZVeqQtKirPHCES6TcTywELJi3raBgSKRt2XKCmHMDzclK9P11qPuOve335Jd8WPISsqbbcw4mIAGQpww7TxIg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-form-layout-item": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.16.0-rc.0.tgz", - "integrity": "sha512-/7oKi/ufCN+2a3STTeAEPunhKq52J7RY0im2Oucy96XU87z1wDIrH3kLtqihX5nzyH6gr2XHksX019tUF4zdWQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.16.0.tgz", + "integrity": "sha512-g1xYut9TQzAK1w0fijWyV2PlXJnaMw3MYgytvsEu3XD93hPut4XvkifM8Ja6YxpkRcKQpRRLa4WHroQ6OQY6LQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-form-validation-message": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-form-validation-message": "1.16.0" } }, "node_modules/@umbraco-ui/uui-form-validation-message": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.16.0-rc.0.tgz", - "integrity": "sha512-t/jvsWTb+KuXSpMjkgUvodmF+r23bmZJXBNyVcE+DPu9CuukCvpNgPJfipndew0312Hjb06nJm1Ht+QqtdqRjw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.16.0.tgz", + "integrity": "sha512-55+WAkF02Im+bG1Xl1AABA7KIGXr5CZTgHbr3MsVVHJMtHv+gQZ04h+0TkvDzKZDSg8ucCXJKyD44Y4gOyS2oA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-icon": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.16.0-rc.0.tgz", - "integrity": "sha512-lSVveGqkEevLgfivHBwxT2atKxEXQhtpA6pJTA3MZ+xpvjx2IS45NML+roN34jDikEIdz9UOTXGuWPHAhd9vOg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.16.0.tgz", + "integrity": "sha512-x7HX9OnKOTgjbFbSSZ9Pk0+Lf6yo8ggLe6XTnPClu3ByN2fl9/QqshI5lx4oz5Adr/ItSj3zqnNB2JbyM56TLA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-icon-registry": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.16.0-rc.0.tgz", - "integrity": "sha512-TBALLQRgaH43XURzNxo/YSnI1Bm3O+1P4of96P1Lgl8BiffLbmOsAZOb9bchasnCpFSmAGtf9xyqY+i3uHw5KQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.16.0.tgz", + "integrity": "sha512-o4l2bEYKdBcxAlSwEPO+cfnNvkGuGcZRyca026xvIz+nufbc/BBzskzS1UWIIjkFPu64rHEfxP/3KbSld64HYA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-icon": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-icon": "1.16.0" } }, "node_modules/@umbraco-ui/uui-icon-registry-essential": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.16.0-rc.0.tgz", - "integrity": "sha512-90Q1YY8/T0mAbPIdyOpFHCl5J9xObV/0WNqGjm35nDpf56Xb3JG3Vt18xYFgT9zUKskEsV6nTjxtGNLrK4+1rA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.16.0.tgz", + "integrity": "sha512-HI4cnYhWpPtWFFgfEltjV6PPhOd3NQ58BhqfbCpRbwmHZUZ0OBzGRl4QgsPNKuhQqmcXene+Twfy8eoRk1/5nQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-icon-registry": "1.16.0" } }, "node_modules/@umbraco-ui/uui-input": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.16.0-rc.0.tgz", - "integrity": "sha512-EUSoym4GUpD94XhIDsjBnQx55Gvd4EjkxqrhPJz9QY5BJ3mjT/l8udcYxtmBeHkFf1vTwJD8eIJcEX/MkPj+yg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.16.0.tgz", + "integrity": "sha512-2Mp15ObjyAuRD3bOTs/zuUHqaaMiuDhmGsjeK8ViOrlSMnz/bVUme5scN1OMkNIryVHkENshC4NK7x6++X0/qw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-input-file": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.16.0-rc.0.tgz", - "integrity": "sha512-IQP5ZeWIrDTWduNiwf6j0GJxl4GwG6KE3CDuAOxjAqt7NJ28AciyaHg1paomZ2mAz+mnvbGt7chwfSuH+Vmb2A==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.16.0.tgz", + "integrity": "sha512-AxepSUJe0LmY4QmBA9UlzhZBBrVF+z88fFUWIH15PICFX0jfsPNIeiwQKlv7cN5pEInUh6qCRN64z8icf8fcdw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-action-bar": "1.16.0-rc.0", - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-button": "1.16.0-rc.0", - "@umbraco-ui/uui-file-dropzone": "1.16.0-rc.0", - "@umbraco-ui/uui-icon": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.16.0-rc.0" + "@umbraco-ui/uui-action-bar": "1.16.0", + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-button": "1.16.0", + "@umbraco-ui/uui-file-dropzone": "1.16.0", + "@umbraco-ui/uui-icon": "1.16.0", + "@umbraco-ui/uui-icon-registry-essential": "1.16.0" } }, "node_modules/@umbraco-ui/uui-input-lock": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.16.0-rc.0.tgz", - "integrity": "sha512-x+Y/yRR3mo+4a0RsOiCcP0KYMp9tESSzkAIETKNyEUuN7aPx6Z0F+3qH40mNMznQ3GBzSnjW8+RsLlNK5rKO5g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.16.0.tgz", + "integrity": "sha512-FTLj/2s+VImEtKe1GPSkAC2pmTabz5cGzvaFB/7xrJj/1evVxXGu8qQyyL96WoDe+RAmBNYfrnGx7OUSVhEyRw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-button": "1.16.0-rc.0", - "@umbraco-ui/uui-icon": "1.16.0-rc.0", - "@umbraco-ui/uui-input": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-button": "1.16.0", + "@umbraco-ui/uui-icon": "1.16.0", + "@umbraco-ui/uui-input": "1.16.0" } }, "node_modules/@umbraco-ui/uui-input-password": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.16.0-rc.0.tgz", - "integrity": "sha512-z7pAOTwPvv3oP8/mV46suC5CB+Wq3EiLVeVBbIktQAWhwhd7LbpaLlNjIq+Cxih/67A6g35PdW8JEsUtqCXNaw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.16.0.tgz", + "integrity": "sha512-0gg8nAVHsMYlQscG76PN4L8ha3CpW15crlzgj4TMaW24OIgZ0khV18ZImJ5n9wv/zrq8LsrwJTyZ5/a/soaKyQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.16.0-rc.0", - "@umbraco-ui/uui-input": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-icon-registry-essential": "1.16.0", + "@umbraco-ui/uui-input": "1.16.0" } }, "node_modules/@umbraco-ui/uui-keyboard-shortcut": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.16.0-rc.0.tgz", - "integrity": "sha512-1MH3TK/FERmFzLFQodJ9Eas7Fl0oyrDKXXdFJiDQGfad9Ai8IZ0FtAqACnKtiGjXqsoxvT/JLQtmfDoLbpHvPQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.16.0.tgz", + "integrity": "sha512-z9wlhONxtwkUCkPEKqt/vSH1qOTwHCIM2Cj/DQ21+bfWcywUR7cAp0vRveapymDn4eHSuRra5lrG7xgLYsYuVg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-label": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.16.0-rc.0.tgz", - "integrity": "sha512-fER5gC8+ptPctkbc4qXkESDe4Pt2DEMZ/pePtI4HGpJV/r2F4FZPaezwBmRvXmoEW7klWT7+7+Je6BSHizKZcA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.16.0.tgz", + "integrity": "sha512-1vQAKUR+frDEth8AMLS5KKpVK2LHD61lWUG95yMypF5C2+YBmzXb70QEakOubTMsmLnYcU3hfORfA5Wp9cYPnw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-loader": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.16.0-rc.0.tgz", - "integrity": "sha512-AUodPdxCky8D8uxvO2N23cE16aK7k+rbJ6Ky0m+H4dI+bZWwJP6fUJHexiEnENURSm6gUGYQv6gXEka0SAWJ2w==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.16.0.tgz", + "integrity": "sha512-wcFUljPcrAR6YYuj5XLmtMpZBvzTBcakr9p+vISOoC3ta8UlE+OOLiQn+XYzTuV/ZbM77EHh5EEyiO5L45fQew==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-loader-bar": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.16.0-rc.0.tgz", - "integrity": "sha512-hVM/YNsrlMWqbGBp1YihtNbZ3IPJCBh4VXWHD4Ht6dHSCQtCOcmB1NVReWsTv6TnAsYClWYrloAqtqqcVdqwUQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.16.0.tgz", + "integrity": "sha512-xh6RCS60WPWPzf0dAA+lTTt0rF8hksQsYBLwITBsR/5k3qswhT9Ctu/2LvqUXoLPyEFTecA4fyqZK+NzhjZrdQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-loader-circle": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.16.0-rc.0.tgz", - "integrity": "sha512-NrOVwTwKdzpJKiH0YfLj3Fm7ye9QXEI6T+K9NKfQafkXKxUo0v8Om7FkEhBBFIbn3YOXrxNL51+EQX3UCb+nww==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.16.0.tgz", + "integrity": "sha512-jawUHoiUwwZkp5YOLFlF00WvZ5yPowfbi22TufSyfls5hMajJM/p21IrCTStrc4ZimqyheaaYe/AqdGLDimfSQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-menu-item": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.16.0-rc.0.tgz", - "integrity": "sha512-l0pfSWj5W6ieNlBxPlntvCadXYIkKQPiD7n9Uy18lirpJcp2NUwlAKbzxaQOFPvUrZtGc9aD+4I6fmRJqWBeZQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.16.0.tgz", + "integrity": "sha512-tyyuehJSj1BU/EEsQ1LHN8eg+gcAKCzqGMwwpepEtKZDd7p1/Ioq1KEn2e20UOihXab5rFv5UNEWSeyEYRqL4Q==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-loader-bar": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-expand": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-loader-bar": "1.16.0", + "@umbraco-ui/uui-symbol-expand": "1.16.0" } }, "node_modules/@umbraco-ui/uui-modal": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.16.0-rc.0.tgz", - "integrity": "sha512-x/IlGqxwED6+JNwOnH9o1+d/PgdPnsBbLOKIRxGl7QDWvwTTrHEezdh6Q9//SBmEsQSVLmvw8ILPYugrN+JZVg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.16.0.tgz", + "integrity": "sha512-hqlXHjlGxEWEeX5c7W0xNlH25xDbb8vdgBIfYGUkBfrYrgO3j+AJ/B7OvmgWJogFTOHRRaPUvKDi8DkDnDH4zw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-pagination": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.16.0-rc.0.tgz", - "integrity": "sha512-NqBliIc726/L9wfPpuXnFASIFHeZ9t6DRhjg/zhwpozsuudjGlvLfrbnm2f4qim6+uvozGwxT83WZ9T83b2biQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.16.0.tgz", + "integrity": "sha512-bZQl5BwiYHSQqc0bjajQbu8ZX+z4qe56t6PiT6s+VUj6huXOOrT72hpY2u+ZE22sAWPaIu42Kg9ulxNV2pulRw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-button": "1.16.0-rc.0", - "@umbraco-ui/uui-button-group": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-button": "1.16.0", + "@umbraco-ui/uui-button-group": "1.16.0" } }, "node_modules/@umbraco-ui/uui-popover": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.16.0-rc.0.tgz", - "integrity": "sha512-zt2n2KYYjTDczgyxw7AJgWgqDYljwBSAwBp5VoEcckx4aDqtgm5u2YmwwapOa8zWj6Nz5O55oPbEGrXsIHc9Rg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.16.0.tgz", + "integrity": "sha512-ZtHPdupRjxwuSHmY5EiiGtZMBi5UsAyHOucn5SxMgdyHT7bRxrV1ebCblDu4eikXg/xx1nTDSFmmW4rXLftULg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-popover-container": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover-container/-/uui-popover-container-1.16.0-rc.0.tgz", - "integrity": "sha512-RDSnm//hsxTDKsklaM4uGk/EOWnxMRjn9dOjY5+S2WH6ehrhn8ULxxKpmvh4J8PQo3cgmlhDxiZLN2Jhx54tbQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover-container/-/uui-popover-container-1.16.0.tgz", + "integrity": "sha512-3N8M4hPQFcthVfqfhdCMX9B4q+0sG2zizoQf2SvDoLp3GAqND2zw2cwYClMy8HJh3XH9JINljz3PliyKMXVaXw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-progress-bar": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.16.0-rc.0.tgz", - "integrity": "sha512-2hJcUx4cli1NYESx6Vo3T0X/EY5XqekcjFBLGRR78zowzZshTiMjOg9CsItsl1pYfO9iYPF5FhH0j0q29bG0lw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.16.0.tgz", + "integrity": "sha512-GE/ZW5Rq82LgVbArppIG8Zkd6QFmCTGEV4Iq5V4KPOl5iSVu2yuYJCDD77aR1LgclSjk1YiJ1/oge94RXqAtOA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-radio": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.16.0-rc.0.tgz", - "integrity": "sha512-nVPLbC2o15EJE1G/R8cscqbwxdq8Rik9CDEy+BSuTEnWvoUXjwZJJYxXswEH1HE+bOSXBzpPYAmDfGFoIYCgNw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.16.0.tgz", + "integrity": "sha512-r3JmVGeGzCzUPEKdOzxunsoRO2q7zGoI5eUtrSXdLSFiR2klW+hti/fjvqvruqzRZRjB0oumbJfMU4IxHcZblw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-range-slider": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.16.0-rc.0.tgz", - "integrity": "sha512-MALLdv/GGdGxe4wRAYcjcgJkFySf98IDunFklLGOY4/fDjGOj4u84Jc01W7EZpLe40IMbY8usjOKpmc/YRfa8A==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.16.0.tgz", + "integrity": "sha512-9qx3Qj8kmIyHRbcVNexWTs4eGjsxs9FkjP7czpC1P0CPJFIt8LzeB6gBwSS/nJGuIo06RQ42qOc8FOza2tN+jA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.16.0-rc.0.tgz", - "integrity": "sha512-Iv9Tx5a6ryIDiI3E8nE5aL51J4+3GoWbmmQnLegNw3y3KcoYfiLVpby5axPFwRKON+Wzk//+AXlyCpJGC11QWw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.16.0.tgz", + "integrity": "sha512-+ptIzEx8a3Oy4XL6TFibR5Q5lWDpjCSPCN2DgIitBj9C0R8zWbBo8sxj2iLGP4RsBiHeTUbDiJlSY1seo2E+Ew==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref-list": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.16.0-rc.0.tgz", - "integrity": "sha512-twilSHIC951ld55HC+S2cvsh4wqmlEcFLdB4ilsvaZs5O8aW1Zrl/T1jDzvqpYRlcFl/jkqLn1vH7hjR9LEC0Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.16.0.tgz", + "integrity": "sha512-MRxTX8CDvquBkkEGfpPsX5ttnsPGJ+Kb1KfR+arueXazQ9XfqyoFCAWWXfOxGL7A5txGTMnKEfj59dyLeCec5Q==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref-node": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.16.0-rc.0.tgz", - "integrity": "sha512-Kb8nrc1z6AaK41W+7rBhZwEJ9vo1c9vjEzY/zzgpT/3RfzaXPAooFT6QoXSbruvm/75jKM4RxbRHEdFo70rZjQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.16.0.tgz", + "integrity": "sha512-4IO02sBoJLlErxXPeFBXTtOZzQeFbCf0flpHCjMZ+vWKZ6GarlUMSvbXjuzh5SBEveVxWYhjd7Z7lP+g2pOHGw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-icon": "1.16.0-rc.0", - "@umbraco-ui/uui-ref": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-icon": "1.16.0", + "@umbraco-ui/uui-ref": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref-node-data-type": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.16.0-rc.0.tgz", - "integrity": "sha512-uf6mJJYHEqbSdi5C5d+tc7VcPNrUbanGF36dW58mMBIkBJoDlB8C05iCkwxSRpr9P1QLgvLst/WQYKH+n0q2bw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.16.0.tgz", + "integrity": "sha512-0yRbSOoKl5gSAnRIEXTdFYlrt4NSvuLx1+TuQyeE/CV8lfObGqM1+y+ueX0AgPuNTXAf7j5rPIRLsVJHfCs2MA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-ref-node": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref-node-document-type": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.16.0-rc.0.tgz", - "integrity": "sha512-nCGqXRM6fYUwzpS0u5cXVmSJoFe3ZCGKu0G9qjoCEDVmKBaW5sAWZdEwWhbPiCVzfKaGQjbO9/DbXsGqZsc6Ag==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.16.0.tgz", + "integrity": "sha512-ORBBH6GRq5VFTNZd++f7dXCLJdgEGhtd1rcdbxjqtYnJrKeJ0dBNhJkF3kLoSQ1MiOG1SHOckGUZr5nLMUhc/w==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-ref-node": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref-node-form": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.16.0-rc.0.tgz", - "integrity": "sha512-bnNs29oxXelvb2rh3CEvWGawk6QkuxB+GBrnnfxLi4KHAHMmL5y6eCUm1gxg9gLtB+iS1ArQdNuHAJfd+BJP8g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.16.0.tgz", + "integrity": "sha512-Z3m2toN+LcZOXVe/3q6d9kyPyWXR9l8CJSk1NkEn/ojMYrRzmo5AW92xWw/twHV8bRsEBDSeKxSKMVGnJVyUHg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-ref-node": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref-node-member": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.16.0-rc.0.tgz", - "integrity": "sha512-zsL1+dytz4a+AxjCIaxLWYHCeLImH/zufkRtNR6lk2XcKSG6uiP3E901CJCMr7LB9yN+pV4A8Y1WKXFL3/rWDQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.16.0.tgz", + "integrity": "sha512-v9m/e5krM1IPV1gI/9dqVKgGYthyWXDlq9lCdiigpTfzv7xkCF+LPEmVksDZaKD498gGYtbYJReCXUxCwjxGTA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-ref-node": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref-node-package": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.16.0-rc.0.tgz", - "integrity": "sha512-EWvIHt0d6gMBR1x2QDLy+rTtyuWZ6w+lnkNl7WXEBUwGO1u/c1YFZVmC6WLivFe0UuROZEObDnmjz/Ai5rwF+w==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.16.0.tgz", + "integrity": "sha512-6z/oa4qX+L746nEet0EDx88roSTcfjnzQj5fH2ebW4WJ6Arh/b+QmPOE3UEn2QiqjJLovkIhNcwf0m9PM7rSSw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-ref-node": "1.16.0" } }, "node_modules/@umbraco-ui/uui-ref-node-user": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.16.0-rc.0.tgz", - "integrity": "sha512-xiuDaxr4/o6X5iCbBx0MfstEIOphXYTBOx4GHtJrBsVBnuw9dGVaod5oTKwV9WyRoACcXt7u/TasLUHcQOJU3A==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.16.0.tgz", + "integrity": "sha512-TdYTh+1pZfOFD9dKBtti1oDF1Pk5Bp3PyNKf1JLtcPm8uD/UPDxRkIYV7It04E6P7VWusdRabdlv/q9PRimA5g==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-ref-node": "1.16.0" } }, "node_modules/@umbraco-ui/uui-scroll-container": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.16.0-rc.0.tgz", - "integrity": "sha512-r4EOhTs+EtDrpi9yBbYRB5fz9fAgdTJU/xSin913k0iL2ciS+ATBZlptCH87KXVpJyydughyiDmvFcQ+4oceDw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.16.0.tgz", + "integrity": "sha512-+ArdQO09sGB1t24rzi+rk3YsZZayZRr5aKny53qAKkklJg0IDCJ+Vme9DvuSk0HBEzCe0YF313lv5mYjxFwCzQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-select": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.16.0-rc.0.tgz", - "integrity": "sha512-tad1sbagCnUwL91jVrbryw6BL3vG0VUf/QMGB8eqkY1CkkETA85RjLn0oMOSpFw2Qbly019pU8pMB8eloftHtg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.16.0.tgz", + "integrity": "sha512-/tXty/HSqTAwnqsmLIsDc8LsE7XW0pZaCu+B/Ov3FjYQSb312AqXBwP7Z59gAbh2M0XvI3qxcA/sLcFndqN1oA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-slider": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.16.0-rc.0.tgz", - "integrity": "sha512-VNyHsqqTIXYO+NptMS1kAzrp1uDGvGg1s0ABmFC+Aln2imCj1m91R0VBccGIK1+zXQvbbK6tTmA4vZ87ozYrug==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.16.0.tgz", + "integrity": "sha512-zWXe+SOzXbhO2tN+DnVXbefEWICZ+FHCR1EGldZdab3hQO53M4HOKqTBd1akE6iFli7FN4BOnELGjnMnupaqvw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-symbol-expand": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.16.0-rc.0.tgz", - "integrity": "sha512-hbIO7WioBAMGuib8KqGLCIiOB4C3yCZAOPEtfUMY8scSvMNtTJ74Z/BJA5AS4vtQmKHROafa7eHlq+tDubZwyA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.16.0.tgz", + "integrity": "sha512-w9i+deCNhZ3TzwgMx2glGbpyvXQHyP0kCmuazXi4cYGFtEXM48d1OScm/PrGs04ICNuqEIwY/IZ+PGfRSI27lA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-symbol-file": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.16.0-rc.0.tgz", - "integrity": "sha512-jJw7W2N+phKv7Sq18skqhWNSnri6tJWfxvHebZ5515Hd1r2ACg9O13Hq5Ih8hrhn8vmpbm4/K7xOpaRnTNh0hg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.16.0.tgz", + "integrity": "sha512-8iyZCjVAFvKrz1m0RTPiZmbXYLyb0Gs2blgg/uPyBzpNvptnXgx29UVTzITu2xvqVvwvureFNcxqeYL5WsfCiA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-symbol-file-dropzone": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.16.0-rc.0.tgz", - "integrity": "sha512-J4Ufk1pC6u6Tz6Ygh1vpXNzJIP8DC8hpk1yNguwsbyX1d85MznQ4JfQfYGmaS9rpY9ypG6FmG39jHzxbPYCQ+A==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.16.0.tgz", + "integrity": "sha512-d9VJQTEBKwTHrvgPAXLgG4m3quDbxg1EhJhE03cxZr/yrZ81I2TD3wd4Pt9uxL1kvpZ95mP2vDfbedUfm/0fww==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-symbol-file-thumbnail": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.16.0-rc.0.tgz", - "integrity": "sha512-B1LmDRCbcSbE7cRCwGtb8MZucd48X1uNfvhfs7CzZdK67P0ngHV0n2Dkujmi3ueAkcd7EMre645IUFI7IG91pQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.16.0.tgz", + "integrity": "sha512-PMm3lTtIAwyE+6Erz2xiamKPuHhqazk2aWHgqC9fzD/0ROlWQMYEP3M99onp8/YCIprzfvXPuH6ofs6kq9bY7Q==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-symbol-folder": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.16.0-rc.0.tgz", - "integrity": "sha512-QpBJTEVUW9ZSPJIgAtm88rqZK3Bcxr/pUN8JDLSH9uVa4azs8jBuvvgETZNQBBG2S9b+xHK34WJ9CQOOJVJK5Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.16.0.tgz", + "integrity": "sha512-vATvt+AcfP9pZxh99DKaq/wrD60EN4nvdtZ/BpHH6MOhX32T8LEboh57XisHmGamUSGbm2jQhASJTt+7cvjI/w==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-symbol-lock": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.16.0-rc.0.tgz", - "integrity": "sha512-eoupPfJdl7YWMllAWf7KmjaEt29choRL0bVsVRJiype9E+5C6BFXPAC6Oa47Rtbo7Pe8SUYkWYQUnmscTMWq0g==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.16.0.tgz", + "integrity": "sha512-mAFnPdUzlddfdLMTkBetCTnShV3QTWMpjqaG5fCaauizWmReye/rCwDur51URL+VkWMIWp29JvfYIIm8Yk+ZGg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-symbol-more": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.16.0-rc.0.tgz", - "integrity": "sha512-1R5GF7QBz56Ogpgx4gOpC3lNGi++cPKxGF3qKwq2IUPfQjCjgXhuzPYyytiLs3/1ONUfnh0O04k+5Tz8DPt7og==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.16.0.tgz", + "integrity": "sha512-WBd/6SNLVP04WU0Em8Uc9/GXsKYpYdHzlEjh7w5oU1TfbDEiNq1lXkOlpuvL79wJtd/2fTKfqui02+i79KU7ig==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-symbol-sort": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.16.0-rc.0.tgz", - "integrity": "sha512-PEfAji9t+9P7JEXLwWWB+B0vhW4VtQgTwdXR1Ncny9fGsPgu1fxkfB+Cwcsn6eJG4jNumu/ftIAR6fdY/nDDQQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.16.0.tgz", + "integrity": "sha512-hBhvUmkPc5WgFcjKDm6jtQq2USCO+ysveJRI1oJReiZkyj06IjU5mYddUL/sOG4L7Ud6OFqVbY002Uw+j9QpYQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-table": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.16.0-rc.0.tgz", - "integrity": "sha512-ub5v+vdQEh2/VMsAqWkHJeMc/hp+HM3Jz61PRMWF0nXDvoapbg1vwOIT5+cfd1QS1dYX4hkP2cyV6JGhIfMIBw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.16.0.tgz", + "integrity": "sha512-cVq84cwbgOvjoTn+5L4eboXPGkYdcIkWm/oU8GxbR1OdUtgPtqnPwB51Ial6ylyIHqvYbCDmDMzrjjnrB/qfJw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-tabs": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.16.0-rc.0.tgz", - "integrity": "sha512-Od2W8ohRfFRzogceMmcAc/Rv2SkA8G2QR1AxukZfBSqin9uR1NUkpknBOy/rt0IvqGhS/cwuX08cU0BXX+tn6A==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.16.0.tgz", + "integrity": "sha512-FBToNg7zgB9paPQPbpnuC66KAMz3iR/F+tmLhjWnwGSit7ubFspPqgrReSjVS9zdd+zbi7wTJOcmKnHmoyP1bw==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-button": "1.16.0-rc.0", - "@umbraco-ui/uui-popover-container": "1.16.0-rc.0", - "@umbraco-ui/uui-symbol-more": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-button": "1.16.0", + "@umbraco-ui/uui-popover-container": "1.16.0", + "@umbraco-ui/uui-symbol-more": "1.16.0" } }, "node_modules/@umbraco-ui/uui-tag": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.16.0-rc.0.tgz", - "integrity": "sha512-PMRDeBN+VkYmiYyNC5jxVKVAH9i2cKuUz5PlaHrQcoKHr6oOui5/zQQaFF2cPasohOVbHCj8RK+vP10/dFuxSg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.16.0.tgz", + "integrity": "sha512-u6pBhOEvXYvUNTxNO1Ftcnflii1CmeuvNAXxuIj8TMmTXGXWmap0W5cGmzlEbbLAMGLv56AJXdz3rKDrWNyTvg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-textarea": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.16.0-rc.0.tgz", - "integrity": "sha512-assLyciYpCfC0gWndyZnG7y54lDeo0V2eB06W/qTMIWIoJ68oPneigwGdkS17jD+XPWHhROWrlh2N4IORSracw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.16.0.tgz", + "integrity": "sha512-xTO4i/m4Q7wEeaxmV1bxT5e1bnLRJ1CoG+awe2FKGq6xw2ZHgksSrm6j3Ddbm5WzV019hIeVl22bnVQ5gOwrww==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@umbraco-ui/uui-toast-notification": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.16.0-rc.0.tgz", - "integrity": "sha512-UDy9pODVkkqZWhAotFuJ8fWtYVBfPmVIHBsNYBMZSRVCZ/zs+WrR+IEew9rDYGiHNew2ZUoGz7C6HfJXIJd7rQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.16.0.tgz", + "integrity": "sha512-ziOJ4uyQpIVCBym2RlZFJOuOb2feNr1sP0RxUjhXToREJdG2MH2bgYyy76K0OCZ7a+JKCsHdaBH4XquXIH93VA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-button": "1.16.0-rc.0", - "@umbraco-ui/uui-css": "1.16.0-rc.0", - "@umbraco-ui/uui-icon": "1.16.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-button": "1.16.0", + "@umbraco-ui/uui-css": "1.16.0", + "@umbraco-ui/uui-icon": "1.16.0", + "@umbraco-ui/uui-icon-registry-essential": "1.16.0" } }, "node_modules/@umbraco-ui/uui-toast-notification-container": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.16.0-rc.0.tgz", - "integrity": "sha512-2Vksc0dDaOzj9WfQfWye8rHrVJqfLq6yboDLWugha0kVBp0d9FyH4jL3nj9sG1YfhBr56pv05ws5Otlr/y7Q6Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.16.0.tgz", + "integrity": "sha512-8HwiYkOA8Rsxpp2ZGsDTq16odV7Ja7xAAp/0BcdosdQYn6L4KUbSimulGaP/Q1KATUCFT7QflQiv0gnwuPpngQ==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-toast-notification": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-toast-notification": "1.16.0" } }, "node_modules/@umbraco-ui/uui-toast-notification-layout": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.16.0-rc.0.tgz", - "integrity": "sha512-u9tCH9eqnVJecnPM0gpxGIb14VbOZHNtLR/9bJauw8P8X34t1mP/6zgnkc9+WL6UiJ0Ljxc5YxzjZfOwA7mbyA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.16.0.tgz", + "integrity": "sha512-OTrTAGUPe8EQRuCWJD8GsCw8MfNJuXx50NLZLDDZKzw3TlDiWMxUD0c4l6zOMy4ih7n7D5sMekHqonW5x6lVuA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-css": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-css": "1.16.0" } }, "node_modules/@umbraco-ui/uui-toggle": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.16.0-rc.0.tgz", - "integrity": "sha512-6OoCkSkfbHo1Rt2INE2/TId7KEb/Cr8Dpra1cIVdIW+zF2zwi1xsY2wf2fK7TZ8QQynjF2cgGneCvEKjVZD5sg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.16.0.tgz", + "integrity": "sha512-opFdwN0LlH6l1xlzEv+e9tvLgySXRr4Ug5LBlzNRJKC/WhinUSq/okerIVyUJgk4oKdZV/y7T7u/07LiekCTAA==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0", - "@umbraco-ui/uui-boolean-input": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0", + "@umbraco-ui/uui-boolean-input": "1.16.0" } }, "node_modules/@umbraco-ui/uui-visually-hidden": { - "version": "1.16.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-visually-hidden/-/uui-visually-hidden-1.16.0-rc.0.tgz", - "integrity": "sha512-O8DvV+5tg9fc+y5zArjVb8EpVD/Spx+2W6P/DX1SsTgf7LJr1mPGxoCYn0Vnd3kK9PgHxRFzDIK1DEJCHXaeQQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-visually-hidden/-/uui-visually-hidden-1.16.0.tgz", + "integrity": "sha512-fqcv9gZUey2FkE2IRWuDgpk+D5XCdC1gnmQ4bIlAs03cMhl2BWP7U04Zo1u78jcWCbjxfnp60rfE6h11ukd5sg==", "license": "MIT", "dependencies": { - "@umbraco-ui/uui-base": "1.16.0-rc.0" + "@umbraco-ui/uui-base": "1.16.0" } }, "node_modules/@vitest/expect": { @@ -17043,8 +17043,8 @@ "src/external/uui": { "name": "@umbraco-backoffice/uui", "dependencies": { - "@umbraco-ui/uui": "^1.16.0-rc.0", - "@umbraco-ui/uui-css": "^1.16.0-rc.0" + "@umbraco-ui/uui": "^1.16.0", + "@umbraco-ui/uui-css": "^1.16.0" } }, "src/packages/block": { diff --git a/src/Umbraco.Web.UI.Client/src/external/uui/package.json b/src/Umbraco.Web.UI.Client/src/external/uui/package.json index 6bc495b29351..a871f79a8e4c 100644 --- a/src/Umbraco.Web.UI.Client/src/external/uui/package.json +++ b/src/Umbraco.Web.UI.Client/src/external/uui/package.json @@ -6,7 +6,7 @@ "build": "vite build" }, "dependencies": { - "@umbraco-ui/uui": "^1.16.0-rc.0", - "@umbraco-ui/uui-css": "^1.16.0-rc.0" + "@umbraco-ui/uui": "^1.16.0", + "@umbraco-ui/uui-css": "^1.16.0" } } From a3a8be47172f9ad6bb64121dc5b4643a44a4b9dc Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 17 Oct 2025 10:46:54 +0200 Subject: [PATCH 045/155] Templates: Retain layout from file when loading template (closes #20524) (#20529) Retain layout from file when loading template. --- .../template-workspace-editor.element.ts | 4 ++-- .../workspace/template-workspace.context.ts | 23 ++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts index a81f4c0f54f4..a40e9c4b06a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts @@ -102,7 +102,7 @@ export class UmbTemplateWorkspaceEditorElement extends UmbLitElement { } #resetMasterTemplate() { - this.#templateWorkspaceContext?.setMasterTemplate(null); + this.#templateWorkspaceContext?.setMasterTemplate(null, true); } #openMasterTemplatePicker() { @@ -121,7 +121,7 @@ export class UmbTemplateWorkspaceEditorElement extends UmbLitElement { ?.onSubmit() .then((value) => { if (!value?.selection) return; - this.#templateWorkspaceContext?.setMasterTemplate(value.selection[0] ?? null); + this.#templateWorkspaceContext?.setMasterTemplate(value.selection[0] ?? null, true); }) .catch(() => undefined); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts index 35ff79bb43d8..0ea8f97d8cdd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts @@ -67,7 +67,15 @@ export class UmbTemplateWorkspaceContext override async load(unique: string) { const response = await super.load(unique); - await this.setMasterTemplate(response.data?.masterTemplate?.unique ?? null); + + // On load we want to set the master template details but not update the layout block in the Razor file. + // This is because you can still set a layout in code by setting `Layout = "_Layout.cshtml";` in the template file. + // This gets set automatically if you create a template under a parent, but you don't have to do that, you can + // just set the `Layout` property in the Razor template file itself. + // So even if there's no master template set by there being a parent, there may still be one set in the Razor + // code, and we shouldn't overwrite it. + await this.setMasterTemplate(response.data?.masterTemplate?.unique ?? null, false); + return response; } @@ -79,9 +87,9 @@ export class UmbTemplateWorkspaceContext }, }); - // Set or reset the master template - // This is important to reset when a new template is created so the UI reflects the correct state - await this.setMasterTemplate(parent.unique); + // On create set or reset the master template depending on whether the template is being created under a parent. + // This is important to reset when a new template is created so the UI reflects the correct state. + await this.setMasterTemplate(parent.unique, true); return data; } @@ -102,7 +110,7 @@ export class UmbTemplateWorkspaceContext return this.getData()?.content ? this.getLayoutBlockRegexPattern().test(this.getData()?.content as string) : false; } - async setMasterTemplate(unique: string | null) { + async setMasterTemplate(unique: string | null, updateLayoutBlock: boolean) { if (unique === null) { this.#masterTemplate.setValue(null); } else { @@ -113,7 +121,10 @@ export class UmbTemplateWorkspaceContext } } - this.#updateMasterTemplateLayoutBlock(); + if (updateLayoutBlock) { + this.#updateMasterTemplateLayoutBlock(); + } + this._data.updateCurrent({ masterTemplate: unique ? { unique } : null }); return unique; From 105cb9da4158d6edd2b3d743926bda22c9ce0d36 Mon Sep 17 00:00:00 2001 From: Anders Reus <88318565+andersreus@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:40:18 +0200 Subject: [PATCH 046/155] Added trashed state so when requesting content from the recycle bin via the management api it will return trashed instead of published state (#20542) Added trashed state so when requesting content from the recycle bin via the management api, the state will be trashed instead of published. --- .../Content/DocumentVariantStateHelper.cs | 9 +++- .../Document/DocumentVariantState.cs | 5 ++ .../DocumentVariantStateHelperTests.cs | 54 ++++++++++--------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelper.cs b/src/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelper.cs index 6d7539b82584..f565b9cdf027 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelper.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelper.cs @@ -12,6 +12,7 @@ internal static DocumentVariantState GetState(IContent content, string? culture) culture, content.Edited, content.Published, + content.Trashed, content.AvailableCultures, content.EditedCultures ?? Enumerable.Empty(), content.PublishedCultures); @@ -22,17 +23,23 @@ internal static DocumentVariantState GetState(IDocumentEntitySlim content, strin culture, content.Edited, content.Published, + content.Trashed, content.CultureNames.Keys, content.EditedCultures, content.PublishedCultures); - private static DocumentVariantState GetState(IEntity entity, string? culture, bool edited, bool published, IEnumerable availableCultures, IEnumerable editedCultures, IEnumerable publishedCultures) + private static DocumentVariantState GetState(IEntity entity, string? culture, bool edited, bool published, bool trashed, IEnumerable availableCultures, IEnumerable editedCultures, IEnumerable publishedCultures) { if (entity.Id <= 0 || (culture is not null && availableCultures.Contains(culture) is false)) { return DocumentVariantState.NotCreated; } + if (trashed) + { + return DocumentVariantState.Trashed; + } + var isDraft = published is false || (culture != null && publishedCultures.Contains(culture) is false); if (isDraft) diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantState.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantState.cs index 3ed51114e100..d3edd54cd93b 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantState.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantState.cs @@ -24,4 +24,9 @@ public enum DocumentVariantState /// The item is published and there are pending changes ///
PublishedPendingChanges = 4, + + /// + /// The item is in the recycle bin + /// + Trashed = 5, } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelperTests.cs index d63eda2eed37..f3c0fe9f7026 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Mapping/Content/DocumentVariantStateHelperTests.cs @@ -11,13 +11,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Mapping.Content [TestFixture] public class DocumentVariantStateHelperTests { - [TestCase(false, false, DocumentVariantState.Draft)] - [TestCase(false, true, DocumentVariantState.Published)] - [TestCase(true, false, DocumentVariantState.Draft)] - [TestCase(true, true, DocumentVariantState.PublishedPendingChanges)] - public void Culture_Invariant_Content_State(bool edited, bool published, DocumentVariantState expectedResult) + [TestCase(false, false, false, DocumentVariantState.Draft)] + [TestCase(false, true, false, DocumentVariantState.Published)] + [TestCase(true, false, false, DocumentVariantState.Draft)] + [TestCase(true, true, false, DocumentVariantState.PublishedPendingChanges)] + [TestCase(true, false, true, DocumentVariantState.Trashed)] + public void Culture_Invariant_Content_State(bool edited, bool published, bool trashed, DocumentVariantState expectedResult) { - var content = Mock.Of(c => c.Id == 1 && c.Published == published && c.Edited == edited); + var content = Mock.Of(c => c.Id == 1 && c.Published == published && c.Edited == edited && c.Trashed == trashed); Assert.AreEqual(expectedResult, DocumentVariantStateHelper.GetState(content, culture: null)); } @@ -31,11 +32,12 @@ public void Culture_Invariant_Content_Not_Created_State(bool edited, bool publis Assert.AreEqual(DocumentVariantState.NotCreated, DocumentVariantStateHelper.GetState(content, culture: null)); } - [TestCase(false, false, DocumentVariantState.Draft)] - [TestCase(false, true, DocumentVariantState.Published)] - [TestCase(true, false, DocumentVariantState.Draft)] - [TestCase(true, true, DocumentVariantState.PublishedPendingChanges)] - public void Culture_Variant_Content_Existing_Culture_State(bool edited, bool published, DocumentVariantState expectedResult) + [TestCase(false, false, false, DocumentVariantState.Draft)] + [TestCase(false, true, false, DocumentVariantState.Published)] + [TestCase(true, false, false, DocumentVariantState.Draft)] + [TestCase(true, true, false, DocumentVariantState.PublishedPendingChanges)] + [TestCase(true, false, true, DocumentVariantState.Trashed)] + public void Culture_Variant_Content_Existing_Culture_State(bool edited, bool published, bool trashed, DocumentVariantState expectedResult) { const string culture = "en"; var content = Mock.Of(c => @@ -43,7 +45,8 @@ public void Culture_Variant_Content_Existing_Culture_State(bool edited, bool pub && c.AvailableCultures == new[] { culture } && c.EditedCultures == (edited ? new[] { culture } : Enumerable.Empty()) && c.Published == published - && c.PublishedCultures == (published ? new[] { culture } : Enumerable.Empty())); + && c.PublishedCultures == (published ? new[] { culture } : Enumerable.Empty()) + && c.Trashed == trashed); Assert.AreEqual(expectedResult, DocumentVariantStateHelper.GetState(content, culture)); } @@ -63,13 +66,14 @@ public void Culture_Variant_Content_Missing_Culture_State(bool edited, bool publ Assert.AreEqual(DocumentVariantState.NotCreated, DocumentVariantStateHelper.GetState(content, "dk")); } - [TestCase(false, false, DocumentVariantState.Draft)] - [TestCase(false, true, DocumentVariantState.Published)] - [TestCase(true, false, DocumentVariantState.Draft)] - [TestCase(true, true, DocumentVariantState.PublishedPendingChanges)] - public void Culture_Invariant_DocumentEntitySlim_State(bool edited, bool published, DocumentVariantState expectedResult) + [TestCase(false, false, false, DocumentVariantState.Draft)] + [TestCase(false, true, false, DocumentVariantState.Published)] + [TestCase(true, false, false, DocumentVariantState.Draft)] + [TestCase(true, true, false, DocumentVariantState.PublishedPendingChanges)] + [TestCase(true, false, true, DocumentVariantState.Trashed)] + public void Culture_Invariant_DocumentEntitySlim_State(bool edited, bool published, bool trashed, DocumentVariantState expectedResult) { - var entity = Mock.Of(c => c.Id == 1 && c.Published == published && c.Edited == edited && c.CultureNames == new Dictionary()); + var entity = Mock.Of(c => c.Id == 1 && c.Published == published && c.Edited == edited && c.CultureNames == new Dictionary() && c.Trashed == trashed); Assert.AreEqual(expectedResult, DocumentVariantStateHelper.GetState(entity, culture: null)); } @@ -83,11 +87,12 @@ public void Culture_Invariant_DocumentEntitySlim_Not_Created_State(bool edited, Assert.AreEqual(DocumentVariantState.NotCreated, DocumentVariantStateHelper.GetState(entity, culture: null)); } - [TestCase(false, false, DocumentVariantState.Draft)] - [TestCase(false, true, DocumentVariantState.Published)] - [TestCase(true, false, DocumentVariantState.Draft)] - [TestCase(true, true, DocumentVariantState.PublishedPendingChanges)] - public void Culture_Variant_DocumentEntitySlim_Existing_Culture_State(bool edited, bool published, DocumentVariantState expectedResult) + [TestCase(false, false, false, DocumentVariantState.Draft)] + [TestCase(false, true, false, DocumentVariantState.Published)] + [TestCase(true, false, false, DocumentVariantState.Draft)] + [TestCase(true, true, false, DocumentVariantState.PublishedPendingChanges)] + [TestCase(true, false, true, DocumentVariantState.Trashed)] + public void Culture_Variant_DocumentEntitySlim_Existing_Culture_State(bool edited, bool published, bool trashed, DocumentVariantState expectedResult) { const string culture = "en"; var entity = Mock.Of(c => @@ -95,7 +100,8 @@ public void Culture_Variant_DocumentEntitySlim_Existing_Culture_State(bool edite && c.CultureNames == new Dictionary { { culture, "value does not matter" } } && c.EditedCultures == (edited ? new[] { culture } : Enumerable.Empty()) && c.Published == published - && c.PublishedCultures == (published ? new[] { culture } : Enumerable.Empty())); + && c.PublishedCultures == (published ? new[] { culture } : Enumerable.Empty()) + && c.Trashed == trashed); Assert.AreEqual(expectedResult, DocumentVariantStateHelper.GetState(entity, culture)); } From 5a65eb1758a28af18efbc7ca3f62ecd1b908333c Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 17 Oct 2025 15:01:09 +0200 Subject: [PATCH 047/155] Update OpenApi.json and client-side models. --- src/Umbraco.Cms.Api.Management/OpenApi.json | 56 +++++++++++++++++-- .../packages/core/backend-api/types.gen.ts | 20 +++++-- .../src/packages/documents/documents/utils.ts | 1 + ...ace-split-view-variant-selector.element.ts | 1 + 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 35de8ff74eee..f4d4118c94d3 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -37322,6 +37322,9 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -37608,6 +37611,9 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -38444,7 +38450,8 @@ "type": "boolean" }, "allowNonExistingSegmentsCreation": { - "type": "boolean" + "type": "boolean", + "deprecated": true } }, "additionalProperties": false @@ -39088,6 +39095,36 @@ }, "additionalProperties": false }, + "DocumentTypePermissionPresentationModel": { + "required": [ + "$type", + "documentTypeAlias", + "verbs" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "verbs": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "documentTypeAlias": { + "type": "string" + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DocumentTypePermissionPresentationModel": "#/components/schemas/DocumentTypePermissionPresentationModel" + } + } + }, "DocumentTypePropertyTypeContainerResponseModel": { "required": [ "id", @@ -39683,7 +39720,8 @@ "NotCreated", "Draft", "Published", - "PublishedPendingChanges" + "PublishedPendingChanges", + "Trashed" ], "type": "string" }, @@ -40178,7 +40216,7 @@ }, "actionParameters": { "type": "object", - "additionalProperties": { }, + "additionalProperties": {}, "nullable": true } }, @@ -40803,7 +40841,7 @@ }, "extensions": { "type": "array", - "items": { } + "items": {} } }, "additionalProperties": false @@ -44640,7 +44678,7 @@ "nullable": true } }, - "additionalProperties": { } + "additionalProperties": {} }, "ProblemDetailsBuilderModel": { "type": "object", @@ -47813,6 +47851,9 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -48250,6 +48291,9 @@ { "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentTypePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -48932,4 +48976,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts index 8050b8555623..94facac6498b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts @@ -412,7 +412,7 @@ export type CreateUserGroupRequestModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; id?: string | null; }; @@ -471,7 +471,7 @@ export type CurrentUserResponseModel = { hasAccessToAllLanguages: boolean; hasAccessToSensitiveData: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; allowedSections: Array; isAdmin: boolean; }; @@ -659,6 +659,9 @@ export type DocumentConfigurationResponseModel = { disableDeleteWhenReferenced: boolean; disableUnpublishWhenReferenced: boolean; allowEditInvariantFromNonDefault: boolean; + /** + * @deprecated + */ allowNonExistingSegmentsCreation: boolean; }; @@ -791,6 +794,12 @@ export type DocumentTypeItemResponseModel = { description?: string | null; }; +export type DocumentTypePermissionPresentationModel = { + $type: string; + verbs: Array; + documentTypeAlias: string; +}; + export type DocumentTypePropertyTypeContainerResponseModel = { id: string; parent?: ReferenceByIdModel | null; @@ -920,7 +929,8 @@ export enum DocumentVariantStateModel { NOT_CREATED = 'NotCreated', DRAFT = 'Draft', PUBLISHED = 'Published', - PUBLISHED_PENDING_CHANGES = 'PublishedPendingChanges' + PUBLISHED_PENDING_CHANGES = 'PublishedPendingChanges', + TRASHED = 'Trashed' } export type DocumentVersionItemResponseModel = { @@ -2822,7 +2832,7 @@ export type UpdateUserGroupRequestModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; }; export type UpdateUserGroupsOnUserRequestModel = { @@ -2923,7 +2933,7 @@ export type UserGroupResponseModel = { mediaStartNode?: ReferenceByIdModel | null; mediaRootAccess: boolean; fallbackPermissions: Array; - permissions: Array; + permissions: Array; id: string; isDeletable: boolean; aliasCanBeChanged: boolean; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts index 3622652b5111..531bca043e55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts @@ -8,6 +8,7 @@ const variantStatesOrder = { [DocumentVariantStateModel.PUBLISHED]: 1, [DocumentVariantStateModel.DRAFT]: 2, [DocumentVariantStateModel.NOT_CREATED]: 3, + [DocumentVariantStateModel.TRASHED]: 4, }; const getVariantStateOrderValue = (variant?: UmbDocumentVariantOptionModel['variant']) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts index 45ef5f33bc7f..5be90a4278b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts @@ -22,6 +22,7 @@ export class UmbDocumentWorkspaceSplitViewVariantSelectorElement extends UmbWork // We should also make our own state model for this [DocumentVariantStateModel.PUBLISHED_PENDING_CHANGES]: 'content_published', [DocumentVariantStateModel.NOT_CREATED]: 'content_notCreated', + [DocumentVariantStateModel.TRASHED]: 'mediaPicker_trashed', }; constructor() { From 8b1f18699d8f5eeb6cbc67fc01a1d24d2f554bd5 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 17 Oct 2025 15:20:01 +0200 Subject: [PATCH 048/155] Update OpenApi.json and client-side models. --- src/Umbraco.Cms.Api.Management/OpenApi.json | 3 ++- .../src/packages/core/backend-api/types.gen.ts | 3 ++- .../src/packages/documents/documents/utils.ts | 1 + .../document-workspace-split-view-variant-selector.element.ts | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 5f13509f568b..51a57abdbd93 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -40166,7 +40166,8 @@ "NotCreated", "Draft", "Published", - "PublishedPendingChanges" + "PublishedPendingChanges", + "Trashed" ], "type": "string" }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts index 0bd3f90ea53e..9ea9d69e5616 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts @@ -910,7 +910,8 @@ export enum DocumentVariantStateModel { NOT_CREATED = 'NotCreated', DRAFT = 'Draft', PUBLISHED = 'Published', - PUBLISHED_PENDING_CHANGES = 'PublishedPendingChanges' + PUBLISHED_PENDING_CHANGES = 'PublishedPendingChanges', + TRASHED = 'Trashed' } export type DocumentVersionItemResponseModel = { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts index 3622652b5111..531bca043e55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/utils.ts @@ -8,6 +8,7 @@ const variantStatesOrder = { [DocumentVariantStateModel.PUBLISHED]: 1, [DocumentVariantStateModel.DRAFT]: 2, [DocumentVariantStateModel.NOT_CREATED]: 3, + [DocumentVariantStateModel.TRASHED]: 4, }; const getVariantStateOrderValue = (variant?: UmbDocumentVariantOptionModel['variant']) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts index 45ef5f33bc7f..5be90a4278b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view-variant-selector.element.ts @@ -22,6 +22,7 @@ export class UmbDocumentWorkspaceSplitViewVariantSelectorElement extends UmbWork // We should also make our own state model for this [DocumentVariantStateModel.PUBLISHED_PENDING_CHANGES]: 'content_published', [DocumentVariantStateModel.NOT_CREATED]: 'content_notCreated', + [DocumentVariantStateModel.TRASHED]: 'mediaPicker_trashed', }; constructor() { From ae2c59b7032e7958ff3a46fc44ab863022cd241f Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 17 Oct 2025 15:45:03 +0200 Subject: [PATCH 049/155] Make the indexing batch size configurable (#20543) * Introduce configurable batch size for indexing * Stop using Examine indexing events for reporting index rebuild operation completeness (it is volatile) --- .../Configuration/Models/IndexingSettings.cs | 6 +++ .../Examine/ContentIndexPopulator.cs | 47 ++++++++++++++++--- .../Examine/DeliveryApiContentIndexHelper.cs | 21 +++++++-- .../Examine/MediaIndexPopulator.cs | 35 +++++++++++--- .../Examine/PublishedContentIndexPopulator.cs | 13 +++++ .../Services/IndexingRebuilderService.cs | 23 --------- .../UmbracoExamine/IndexInitializer.cs | 10 ++-- 7 files changed, 113 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/IndexingSettings.cs b/src/Umbraco.Core/Configuration/Models/IndexingSettings.cs index ff3bebd9894a..282d6c57fb8e 100644 --- a/src/Umbraco.Core/Configuration/Models/IndexingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/IndexingSettings.cs @@ -9,10 +9,16 @@ namespace Umbraco.Cms.Core.Configuration.Models; public class IndexingSettings { private const bool StaticExplicitlyIndexEachNestedProperty = false; + private const int StaticBatchSize = 10000; /// /// Gets or sets a value for whether each nested property should have it's own indexed value. Requires a rebuild of indexes when changed. /// [DefaultValue(StaticExplicitlyIndexEachNestedProperty)] public bool ExplicitlyIndexEachNestedProperty { get; set; } = StaticExplicitlyIndexEachNestedProperty; + + /// + /// Gets or sets a value for how many items to index at a time. + /// + public int BatchSize { get; set; } = StaticBatchSize; } diff --git a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs index eb9e9f135fdd..e912728feef7 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs @@ -1,5 +1,9 @@ using Examine; +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.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; @@ -20,11 +24,23 @@ public class ContentIndexPopulator : IndexPopulator private readonly bool _publishedValuesOnly; private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + private IndexingSettings _indexingSettings; + /// /// This is a static query, it's parameters don't change so store statically /// private IQuery? _publishedQuery; + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V19.")] + public ContentIndexPopulator( + ILogger logger, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IContentValueSetBuilder contentValueSetBuilder) + : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + /// /// Default constructor to lookup all content data /// @@ -32,8 +48,21 @@ public ContentIndexPopulator( ILogger logger, IContentService contentService, IUmbracoDatabaseFactory umbracoDatabaseFactory, - IContentValueSetBuilder contentValueSetBuilder) - : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) + IContentValueSetBuilder contentValueSetBuilder, + IOptionsMonitor indexingSettings) + : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder, indexingSettings) + { + } + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V19.")] + public ContentIndexPopulator( + ILogger logger, + bool publishedValuesOnly, + int? parentId, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IValueSetBuilder contentValueSetBuilder) + : this(logger, publishedValuesOnly, parentId, contentService, umbracoDatabaseFactory, contentValueSetBuilder, StaticServiceProvider.Instance.GetRequiredService>()) { } @@ -46,7 +75,8 @@ public ContentIndexPopulator( int? parentId, IContentService contentService, IUmbracoDatabaseFactory umbracoDatabaseFactory, - IValueSetBuilder contentValueSetBuilder) + IValueSetBuilder contentValueSetBuilder, + IOptionsMonitor indexingSettings) { _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _umbracoDatabaseFactory = umbracoDatabaseFactory ?? throw new ArgumentNullException(nameof(umbracoDatabaseFactory)); @@ -54,6 +84,12 @@ public ContentIndexPopulator( _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _publishedValuesOnly = publishedValuesOnly; _parentId = parentId; + _indexingSettings = indexingSettings.CurrentValue; + + indexingSettings.OnChange(change => + { + _indexingSettings = change; + }); } private IQuery PublishedQuery => _publishedQuery ??= @@ -75,7 +111,6 @@ protected override void PopulateIndexes(IReadOnlyList indexes) return; } - const int pageSize = 10000; var pageIndex = 0; var contentParentId = -1; @@ -86,11 +121,11 @@ protected override void PopulateIndexes(IReadOnlyList indexes) if (_publishedValuesOnly) { - IndexPublishedContent(contentParentId, pageIndex, pageSize, indexes); + IndexPublishedContent(contentParentId, pageIndex, _indexingSettings.BatchSize, indexes); } else { - IndexAllContent(contentParentId, pageIndex, pageSize, indexes); + IndexAllContent(contentParentId, pageIndex, _indexingSettings.BatchSize, indexes); } } diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelper.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelper.cs index d7f2fc0c69af..2747773e1134 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelper.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelper.cs @@ -1,5 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; @@ -14,22 +16,33 @@ internal sealed class DeliveryApiContentIndexHelper : IDeliveryApiContentIndexHe private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; private DeliveryApiSettings _deliveryApiSettings; + private IndexingSettings _indexingSettings; + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V19.")] public DeliveryApiContentIndexHelper( IContentService contentService, IUmbracoDatabaseFactory umbracoDatabaseFactory, IOptionsMonitor deliveryApiSettings) + : this(contentService, umbracoDatabaseFactory, deliveryApiSettings, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public DeliveryApiContentIndexHelper( + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IOptionsMonitor deliveryApiSettings, + IOptionsMonitor indexingSettings) { _contentService = contentService; _umbracoDatabaseFactory = umbracoDatabaseFactory; _deliveryApiSettings = deliveryApiSettings.CurrentValue; + _indexingSettings = indexingSettings.CurrentValue; deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings); + indexingSettings.OnChange(settings => _indexingSettings = settings); } public void EnumerateApplicableDescendantsForContentIndex(int rootContentId, Action actionToPerform) - { - const int pageSize = 10000; - EnumerateApplicableDescendantsForContentIndex(rootContentId, actionToPerform, pageSize); - } + => EnumerateApplicableDescendantsForContentIndex(rootContentId, actionToPerform, _indexingSettings.BatchSize); internal void EnumerateApplicableDescendantsForContentIndex(int rootContentId, Action actionToPerform, int pageSize) { diff --git a/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs index 6f4a4db4a3a9..b9872da5c8ae 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs @@ -1,5 +1,9 @@ using Examine; +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.Models; using Umbraco.Cms.Core.Services; @@ -15,23 +19,43 @@ public class MediaIndexPopulator : IndexPopulator private readonly IValueSetBuilder _mediaValueSetBuilder; private readonly int? _parentId; + private IndexingSettings _indexingSettings; + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V19.")] + public MediaIndexPopulator(ILogger logger, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + : this(logger, null, mediaService, mediaValueSetBuilder, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + /// /// Default constructor to lookup all content data /// - public MediaIndexPopulator(ILogger logger, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) - : this(logger, null, mediaService, mediaValueSetBuilder) + public MediaIndexPopulator(ILogger logger, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder, IOptionsMonitor indexingSettings) + : this(logger, null, mediaService, mediaValueSetBuilder, indexingSettings) + { + } + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V19.")] + public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + : this(logger, parentId, mediaService, mediaValueSetBuilder, StaticServiceProvider.Instance.GetRequiredService>()) { } /// /// Optional constructor allowing specifying custom query parameters /// - public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder, IOptionsMonitor indexingSettings) { _logger = logger; _parentId = parentId; _mediaService = mediaService; _mediaValueSetBuilder = mediaValueSetBuilder; + _indexingSettings = indexingSettings.CurrentValue; + + indexingSettings.OnChange(change => + { + _indexingSettings = change; + }); } protected override void PopulateIndexes(IReadOnlyList indexes) @@ -46,7 +70,6 @@ protected override void PopulateIndexes(IReadOnlyList indexes) return; } - const int pageSize = 10000; var pageIndex = 0; var mediaParentId = -1; @@ -60,7 +83,7 @@ protected override void PopulateIndexes(IReadOnlyList indexes) do { - media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out _).ToArray(); + media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, _indexingSettings.BatchSize, out _).ToArray(); // ReSharper disable once PossibleMultipleEnumeration foreach (IIndex index in indexes) @@ -70,6 +93,6 @@ protected override void PopulateIndexes(IReadOnlyList indexes) pageIndex++; } - while (media.Length == pageSize); + while (media.Length == _indexingSettings.BatchSize); } } diff --git a/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs index 67d59d02d9c8..4f07a7283f03 100644 --- a/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; @@ -15,6 +17,7 @@ namespace Umbraco.Cms.Infrastructure.Examine; /// public class PublishedContentIndexPopulator : ContentIndexPopulator { + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V19.")] public PublishedContentIndexPopulator( ILogger logger, IContentService contentService, @@ -23,4 +26,14 @@ public PublishedContentIndexPopulator( : base(logger, true, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) { } + + public PublishedContentIndexPopulator( + ILogger logger, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IPublishedContentValueSetBuilder contentValueSetBuilder, + IOptionsMonitor indexingSettings) + : base(logger, true, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder, indexingSettings) + { + } } diff --git a/src/Umbraco.Infrastructure/Services/IndexingRebuilderService.cs b/src/Umbraco.Infrastructure/Services/IndexingRebuilderService.cs index 1263f52e5e4c..aaab2497ffb0 100644 --- a/src/Umbraco.Infrastructure/Services/IndexingRebuilderService.cs +++ b/src/Umbraco.Infrastructure/Services/IndexingRebuilderService.cs @@ -42,12 +42,6 @@ public bool TryRebuild(IIndex index, string indexName) /// public async Task TryRebuildAsync(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 { Attempt attempt = await _indexRebuilder.RebuildIndexAsync(indexName); @@ -55,8 +49,6 @@ public async Task TryRebuildAsync(IIndex index, string indexName) } catch (Exception exception) { - // Ensure it's not listening - index.IndexOperationComplete -= Indexer_IndexOperationComplete; _logger.LogError(exception, "An error occurred rebuilding index"); return false; } @@ -70,19 +62,4 @@ public bool IsRebuilding(string indexName) /// public Task IsRebuildingAsync(string indexName) => _indexRebuilder.IsRebuildingAsync(indexName); - - 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 '{IndexerName}' done.", indexer?.Name); - } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs index 1d7cf2e8418e..0b1f6029073e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs @@ -39,6 +39,7 @@ public class IndexInitializer private readonly IContentTypeService _contentTypeService; private readonly IDocumentUrlService _documentUrlService; private readonly ILanguageService _languageService; + private readonly IOptionsMonitor _indexSettings; public IndexInitializer( IShortStringHelper shortStringHelper, @@ -50,7 +51,8 @@ public IndexInitializer( ILocalizationService localizationService, IContentTypeService contentTypeService, IDocumentUrlService documentUrlService, - ILanguageService languageService) + ILanguageService languageService, + IOptionsMonitor indexSettings) { _shortStringHelper = shortStringHelper; _propertyEditors = propertyEditors; @@ -62,6 +64,7 @@ public IndexInitializer( _contentTypeService = contentTypeService; _documentUrlService = documentUrlService; _languageService = languageService; + _indexSettings = indexSettings; } public ContentValueSetBuilder GetContentValueSetBuilder(bool publishedValuesOnly) @@ -91,7 +94,8 @@ public ContentIndexPopulator GetContentIndexRebuilder(IContentService contentSer null, contentService, umbracoDatabaseFactory, - contentValueSetBuilder); + contentValueSetBuilder, + _indexSettings); return contentIndexDataSource; } @@ -105,7 +109,7 @@ public MediaIndexPopulator GetMediaIndexRebuilder(IMediaService mediaService) _shortStringHelper, _contentSettings, StaticServiceProvider.Instance.GetRequiredService()); - var mediaIndexDataSource = new MediaIndexPopulator(null, mediaService, mediaValueSetBuilder); + var mediaIndexDataSource = new MediaIndexPopulator(null, mediaService, mediaValueSetBuilder, _indexSettings); return mediaIndexDataSource; } From d5a2f0572edf9a842c05247cde3a8f9b6285c0e8 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Mon, 20 Oct 2025 10:51:38 +0100 Subject: [PATCH 050/155] Preview: Redirect to published URL on exit (#20556) * Preview Exit: Gets the page's published URL on exit for redirect * Preview Open Website: Uses the page's published URL * Tweaked the published URL logic * Code amends based on @copilot's suggestions --- .../apps/preview/apps/preview-exit.element.ts | 2 +- .../apps/preview-open-website.element.ts | 2 +- .../src/apps/preview/preview.context.ts | 35 ++++++++++++++++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts index 95e907a38a87..26af65490ba7 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts @@ -6,7 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; export class UmbPreviewExitElement extends UmbLitElement { async #onClick() { const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT); - previewContext?.exitPreview(0); + await previewContext?.exitPreview(0); } override render() { diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts index aba28768e5c1..4a77454df8f5 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts @@ -6,7 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; export class UmbPreviewOpenWebsiteElement extends UmbLitElement { async #onClick() { const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT); - previewContext?.openWebsite(); + await previewContext?.openWebsite(); } override render() { diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts index 21899e41f859..75de96a38c5f 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts @@ -1,10 +1,12 @@ -import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { tryExecute } from '@umbraco-cms/backoffice/resources'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions'; @@ -89,6 +91,19 @@ export class UmbPreviewContext extends UmbContextBase { }); } + async #getPublishedUrl(): Promise { + if (!this.#unique) return null; + + // NOTE: We should be reusing `UmbDocumentUrlRepository` here, but the preview app doesn't register the `itemStore` extensions, so can't resolve/consume `UMB_DOCUMENT_URL_STORE_CONTEXT`. [LK] + const { data } = await tryExecute(this, DocumentService.getDocumentUrls({ query: { id: [this.#unique] } })); + + if (!data?.length) return null; + const urlInfo = this.#culture ? data[0].urlInfos.find((x) => x.culture === this.#culture) : data[0].urlInfos[0]; + + if (!urlInfo?.url) return null; + return urlInfo.url.startsWith('/') ? `${this.#serverUrl}${urlInfo.url}` : urlInfo.url; + } + #getSessionCount(): number { return Math.max(Number(localStorage.getItem(UMB_LOCALSTORAGE_SESSION_KEY)), 0) || 0; } @@ -170,7 +185,12 @@ export class UmbPreviewContext extends UmbContextBase { this.#webSocket = undefined; } - const url = this.#previewUrl.getValue() as string; + let url = await this.#getPublishedUrl(); + + if (!url) { + url = this.#previewUrl.getValue() as string; + } + window.location.replace(url); } @@ -190,8 +210,13 @@ export class UmbPreviewContext extends UmbContextBase { return this.getHostElement().shadowRoot?.querySelector('#wrapper') as HTMLElement; } - openWebsite() { - const url = this.#previewUrl.getValue() as string; + async openWebsite() { + let url = await this.#getPublishedUrl(); + + if (!url) { + url = this.#previewUrl.getValue() as string; + } + window.open(url, '_blank'); } From 7751e40ba859be33f3eb8924329c8ccd60961c91 Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:50:13 +0700 Subject: [PATCH 051/155] E2E: QA Fixed the flaky tests related to publishing content with image cropper (#20577) Added more waits --- .../tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts index 717fc227c30f..21da07d52fce 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts @@ -60,6 +60,8 @@ test('can publish content with the image cropper data type', {tag: '@smoke'}, as // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.uploadFile(imageFilePath); + // Wait for the upload to complete + await umbracoUi.waitForTimeout(1000); await umbracoUi.content.clickSaveAndPublishButton(); // Assert From 5337c38f2c7e8126b7a247dcc5710d82ea6a221f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:56:59 +0000 Subject: [PATCH 052/155] Bump vite from 7.1.9 to 7.1.11 in /src/Umbraco.Web.UI.Client Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.9 to 7.1.11. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.1.11 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- src/Umbraco.Web.UI.Client/package-lock.json | 8 ++++---- src/Umbraco.Web.UI.Client/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index b2657a2a273b..cf63f6c06461 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -58,7 +58,7 @@ "typescript": "5.9.3", "typescript-eslint": "^8.45.0", "typescript-json-schema": "^0.65.1", - "vite": "^7.1.9", + "vite": "^7.1.11", "vite-plugin-static-copy": "^3.1.3", "vite-tsconfig-paths": "^5.1.4", "web-component-analyzer": "^2.0.0" @@ -16364,9 +16364,9 @@ } }, "node_modules/vite": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", - "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index d9145b2732a8..f9ce4923ea42 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -261,7 +261,7 @@ "typescript": "5.9.3", "typescript-eslint": "^8.45.0", "typescript-json-schema": "^0.65.1", - "vite": "^7.1.9", + "vite": "^7.1.11", "vite-plugin-static-copy": "^3.1.3", "vite-tsconfig-paths": "^5.1.4", "web-component-analyzer": "^2.0.0" From ae41438a366aec1198d3529b4c4051f10d05201c Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Tue, 21 Oct 2025 08:28:01 +0100 Subject: [PATCH 053/155] Tiptap RTE: Allow removal of unregistered extensions (#20571) * Tiptap toolbar config: enable removal of unregistered extensions * Tiptap statusbar config: enable removal of unregistered extensions * Tiptap toolbar config: Typescript tidy-up * Tiptap toolbar sorting amend Removed the need for the `tiptap-toolbar-alias` attribute, we can reuse the `data-mark`. * Tiptap extension config UI amend If the extension doesn't have a `description`, then add the `alias` to the title/tooltip, to give a DX hint. * Tiptap toolbar: adds `title` to placeholder skeleton * Added missing `forExtensions` for Style Select and Horizontal Rule toolbar extensions * Update src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/property-editor-ui-tiptap-statusbar-configuration.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../toolbar/tiptap-toolbar.element.ts | 6 +- .../extensions/horizontal-rule/manifests.ts | 1 + .../extensions/style-select/manifests.ts | 1 + ...tiptap-extensions-configuration.element.ts | 14 +--- ...-tiptap-statusbar-configuration.element.ts | 64 +++++++++++-------- .../tiptap-statusbar-configuration.context.ts | 5 +- ...ui-tiptap-toolbar-configuration.element.ts | 4 +- .../tiptap-toolbar-configuration.context.ts | 4 +- ...tap-toolbar-group-configuration.element.ts | 42 ++++++------ .../packages/tiptap/property-editors/types.ts | 9 +-- 10 files changed, 80 insertions(+), 70 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar.element.ts index 50e548eec36b..0627c2c3e7ac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar.element.ts @@ -92,11 +92,11 @@ export class UmbTiptapToolbarElement extends UmbLitElement { } #renderActions(aliases: Array) { - return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? this.#renderActionPlaceholder()); + return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? this.#renderActionPlaceholder(alias)); } - #renderActionPlaceholder() { - return html``; + #renderActionPlaceholder(alias: string) { + return html``; } static override readonly styles = css` diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts index 47523bb921b5..86ec324126d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts @@ -16,6 +16,7 @@ export const manifests: Array = [ alias: 'Umb.Tiptap.Toolbar.HorizontalRule', name: 'Horizontal Rule Tiptap Toolbar Extension', api: () => import('./horizontal-rule.tiptap-toolbar-api.js'), + forExtensions: ['Umb.Tiptap.HorizontalRule'], meta: { alias: 'horizontalRule', icon: 'icon-horizontal-rule', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts index 60c0894e842a..b6a98f1ddf5f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts @@ -4,6 +4,7 @@ export const manifests: Array = [ kind: 'styleMenu', alias: 'Umb.Tiptap.Toolbar.StyleSelect', name: 'Style Select Tiptap Extension', + forExtensions: ['Umb.Tiptap.Heading', 'Umb.Tiptap.Blockquote', 'Umb.Tiptap.CodeBlock'], items: [ { label: 'Headers', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/extensions-configuration/property-editor-ui-tiptap-extensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/extensions-configuration/property-editor-ui-tiptap-extensions-configuration.element.ts index ad71fe55681f..89ac81a68f9b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/extensions-configuration/property-editor-ui-tiptap-extensions-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/extensions-configuration/property-editor-ui-tiptap-extensions-configuration.element.ts @@ -1,14 +1,4 @@ -import { - css, - customElement, - html, - ifDefined, - nothing, - property, - state, - repeat, - when, -} from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, state, repeat, when } from '@umbraco-cms/backoffice/external/lit'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; @@ -166,7 +156,7 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement ${repeat( group.extensions, (item) => html` -
  • +
  • this.#context.removeStatusbarItem([areaIndex, itemIndex])} - @dragend=${this.#onDragEnd} - @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [areaIndex, itemIndex])}> -
    - ${when(item.icon, (icon) => html``)} - ${label} -
    - - `; + const label = this.localize.string(item.label) || item.alias; + + switch (item.kind) { + case 'unknown': + return html` + this.#context.removeStatusbarItem([areaIndex, itemIndex])}> + `; + + default: + return html` + this.#context.removeStatusbarItem([areaIndex, itemIndex])} + @dragend=${this.#onDragEnd} + @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [areaIndex, itemIndex])}> +
    + ${when(item.icon, (icon) => html``)} + ${label} +
    +
    + `; + } } static override readonly styles = [ @@ -303,8 +317,8 @@ export class UmbPropertyEditorUiTiptapStatusbarConfigurationElement --color-standalone: var(--uui-color-danger-standalone); --color-emphasis: var(--uui-color-danger-emphasis); --color-contrast: var(--uui-color-danger); - --uui-button-contrast-disabled: var(--uui-color-danger); - --uui-button-border-color-disabled: var(--uui-color-danger); + --uui-button-contrast: var(--uui-color-danger); + --uui-button-border-color: var(--uui-color-danger); } div { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/tiptap-statusbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/tiptap-statusbar-configuration.context.ts index 8b3e29cf6eae..84844d673f7a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/tiptap-statusbar-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/tiptap-statusbar-configuration.context.ts @@ -31,6 +31,7 @@ export class UmbTiptapStatusbarConfigurationContext extends UmbContextBase { const _extensions = extensions .sort((a, b) => a.alias.localeCompare(b.alias)) .map((ext) => ({ + kind: 'default', alias: ext.alias, label: ext.meta.label, icon: ext.meta.icon, @@ -75,8 +76,8 @@ export class UmbTiptapStatusbarConfigurationContext extends UmbContextBase { .filter((ext) => ext.alias?.toLowerCase().includes(query) || ext.label?.toLowerCase().includes(query)); } - public getExtensionByAlias(alias: string): UmbTiptapStatusbarExtension | undefined { - return this.#lookup?.get(alias); + public getExtensionByAlias(alias: string): UmbTiptapStatusbarExtension { + return this.#lookup?.get(alias) ?? { label: '', alias, icon: '', kind: 'unknown' }; } public isExtensionEnabled(alias: string): boolean { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts index bb000b6d7c96..cebfc2463c07 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts @@ -255,9 +255,7 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement #renderGroup(group?: UmbTiptapToolbarGroupViewModel, rowIndex = 0, groupIndex = 0) { if (!group) return nothing; const showActionBar = this._toolbar[rowIndex].data.length > 1 && group.data.length === 0; - const items: UmbTiptapToolbarExtension[] = group!.data - .map((alias) => this.#context?.getExtensionByAlias(alias)) - .filter((item): item is UmbTiptapToolbarExtension => !!item); + const items = group.data.map((alias) => this.#context?.getExtensionByAlias(alias)); return html`
    ext.alias?.toLowerCase().includes(query) || ext.label?.toLowerCase().includes(query)); } - public getExtensionByAlias(alias: string): UmbTiptapToolbarExtension | undefined { - return this.#lookup?.get(alias); + public getExtensionByAlias(alias: string): UmbTiptapToolbarExtension { + return this.#lookup?.get(alias) ?? { label: '', alias, icon: '', kind: 'unknown' }; } public isExtensionEnabled(alias: string): boolean { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts index cc7b02d45c37..dd8eba4ac8da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts @@ -10,9 +10,9 @@ export class UmbTiptapToolbarGroupConfigurationElement< TiptapToolbarItem extends UmbTiptapToolbarExtension = UmbTiptapToolbarExtension, > extends UmbLitElement { #sorter = new UmbSorterController(this, { - getUniqueOfElement: (element) => element.getAttribute('tiptap-toolbar-alias'), - getUniqueOfModel: (modelEntry) => modelEntry.alias!, - itemSelector: 'uui-button', + getUniqueOfElement: (element) => element.dataset.mark, + getUniqueOfModel: (modelEntry) => `tiptap-toolbar-item:${modelEntry.alias}`, + itemSelector: '.draggable', identifier: 'umb-tiptap-toolbar-sorter', containerSelector: '.items', resolvePlacement: UmbSorterResolvePlacementAsGrid, @@ -71,7 +71,7 @@ export class UmbTiptapToolbarGroupConfigurationElement< } #renderItem(item: TiptapToolbarItem, index = 0) { - const label = this.localize.string(item.label); + const label = this.localize.string(item.label) || item.alias; const forbidden = !this.#context?.isExtensionEnabled(item.alias); switch (item.kind) { @@ -80,13 +80,11 @@ export class UmbTiptapToolbarGroupConfigurationElement< return html` this.#onRequestRemove(item, index)}>
    ${label} @@ -95,18 +93,29 @@ export class UmbTiptapToolbarGroupConfigurationElement< `; + case 'unknown': + return html` + this.#onRequestRemove(item, index)}> + `; + case 'button': + case 'colorPickerButton': default: return html` this.#onRequestRemove(item, index)}>
    ${when( @@ -131,23 +140,18 @@ export class UmbTiptapToolbarGroupConfigurationElement< uui-button { --uui-button-font-weight: normal; - &[draggable='true'], - &[draggable='true'] > .inner { + &.draggable, + &.draggable > .inner { cursor: move; } - &[disabled], - &[disabled] > .inner { - cursor: not-allowed; - } - &.forbidden { --color: var(--uui-color-danger); --color-standalone: var(--uui-color-danger-standalone); --color-emphasis: var(--uui-color-danger-emphasis); --color-contrast: var(--uui-color-danger); - --uui-button-contrast-disabled: var(--uui-color-danger); - --uui-button-border-color-disabled: var(--uui-color-danger); + --uui-button-contrast: var(--uui-color-danger); + --uui-button-border-color: var(--uui-color-danger); } div { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/types.ts index c7c80b8c23a9..a6572054c925 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/types.ts @@ -1,17 +1,18 @@ export type UmbTiptapSortableViewModel = { unique: string; data: T }; -export type UmbTiptapStatusbarExtension = { +export type UmbTiptapExtensionBase = { + kind?: string; alias: string; label: string; icon: string; dependencies?: Array; }; +export type UmbTiptapStatusbarExtension = UmbTiptapExtensionBase; + export type UmbTiptapStatusbarViewModel = UmbTiptapSortableViewModel>; -export type UmbTiptapToolbarExtension = UmbTiptapStatusbarExtension & { - kind?: string; -}; +export type UmbTiptapToolbarExtension = UmbTiptapExtensionBase; export type UmbTiptapToolbarRowViewModel = UmbTiptapSortableViewModel>; export type UmbTiptapToolbarGroupViewModel = UmbTiptapSortableViewModel>; From 81a8a0c191a56921e2bda536e50d01e95910324d Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 21 Oct 2025 09:57:29 +0200 Subject: [PATCH 054/155] Hybrid Cache: Resolve start-up errors with mis-matched types (#20554) * Be consistent in use of GetOrCreateAsync overload in exists and retrieval. Ensure nullability of ContentCacheNode is consistent in exists and retrieval. * Applied suggestion from code review. * Move seeding to Umbraco application starting rather than started, ensuring an initial request is served. * Tighten up hybrid cache exists check with locking around check and remove, and use of cancellation token. --- .../UmbracoBuilderExtensions.cs | 3 +- .../Extensions/HybridCacheExtensions.cs | 67 +++++++++++++------ .../SeedingNotificationHandler.cs | 6 +- .../Services/DocumentCacheService.cs | 4 +- .../Services/MediaCacheService.cs | 4 +- .../DocumentHybridCacheTests.cs | 60 ++++++++++++++++- .../Extensions/HybridCacheExtensionsTests.cs | 55 +++++++-------- 7 files changed, 141 insertions(+), 58 deletions(-) diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs index ca625dacdf5c..e97c60fd6253 100644 --- a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,4 +1,3 @@ - using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -74,7 +73,7 @@ public static IUmbracoBuilder AddUmbracoHybridCache(this IUmbracoBuilder builder builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); builder.AddCacheSeeding(); return builder; } diff --git a/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs index ee1b7aefda05..ccd5897494eb 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Microsoft.Extensions.Caching.Hybrid; namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions; @@ -7,19 +8,24 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions; /// internal static class HybridCacheExtensions { + // Per-key semaphores to ensure the GetOrCreateAsync + RemoveAsync sequence + // executes atomically for a given cache key. + private static readonly ConcurrentDictionary _keyLocks = new(); + /// /// Returns true if the cache contains an item with a matching key. /// /// An instance of /// The name (key) of the item to search for in the cache. + /// The cancellation token. /// True if the item exists already. False if it doesn't. /// /// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191 /// Will never add or alter the state of any items in the cache. /// - public static async Task ExistsAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key) + public static async Task ExistsAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token) { - (bool exists, _) = await TryGetValueAsync(cache, key); + (bool exists, _) = await TryGetValueAsync(cache, key, token).ConfigureAwait(false); return exists; } @@ -29,34 +35,55 @@ public static async Task ExistsAsync(this Microsoft.Extensions.Caching. /// The type of the value of the item in the cache. /// An instance of /// The name (key) of the item to search for in the cache. + /// The cancellation token. /// A tuple of and the object (if found) retrieved from the cache. /// /// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191 /// Will never add or alter the state of any items in the cache. /// - public static async Task<(bool Exists, T? Value)> TryGetValueAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key) + public static async Task<(bool Exists, T? Value)> TryGetValueAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token) { var exists = true; - T? result = await cache.GetOrCreateAsync( - key, - null!, - (_, _) => - { - exists = false; - return new ValueTask(default(T)!); - }, - new HybridCacheEntryOptions(), - null, - CancellationToken.None); - - // In checking for the existence of the item, if not found, we will have created a cache entry with a null value. - // So remove it again. - if (exists is false) + // Acquire a per-key semaphore so that GetOrCreateAsync and the possible RemoveAsync + // complete without another thread retrieving/creating the same key in-between. + SemaphoreSlim sem = _keyLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); + + await sem.WaitAsync().ConfigureAwait(false); + + try { - await cache.RemoveAsync(key); + T? result = await cache.GetOrCreateAsync( + key, + cancellationToken => + { + exists = false; + return default; + }, + new HybridCacheEntryOptions(), + null, + token).ConfigureAwait(false); + + // In checking for the existence of the item, if not found, we will have created a cache entry with a null value. + // So remove it again. Because we're holding the per-key lock there is no chance another thread + // will observe the temporary entry between GetOrCreateAsync and RemoveAsync. + if (exists is false) + { + await cache.RemoveAsync(key).ConfigureAwait(false); + } + + return (exists, result); } + finally + { + sem.Release(); - return (exists, result); + // Only remove the semaphore mapping if it still points to the same instance we used. + // This avoids removing another thread's semaphore or corrupting the map. + if (_keyLocks.TryGetValue(key, out SemaphoreSlim? current) && ReferenceEquals(current, sem)) + { + _keyLocks.TryRemove(key, out _); + } + } } } diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs index 0581bd26542c..38a6618c7019 100644 --- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; -internal sealed class SeedingNotificationHandler : INotificationAsyncHandler +internal sealed class SeedingNotificationHandler : INotificationAsyncHandler { private readonly IDocumentCacheService _documentCacheService; private readonly IMediaCacheService _mediaCacheService; @@ -24,7 +24,7 @@ public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IM _globalSettings = globalSettings.Value; } - public async Task HandleAsync(UmbracoApplicationStartedNotification notification, + public async Task HandleAsync(UmbracoApplicationStartingNotification notification, CancellationToken cancellationToken) { diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index 1675cb05cfb2..2879be5c8d7f 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -205,7 +205,7 @@ public async Task SeedAsync(CancellationToken cancellationToken) var cacheKey = GetCacheKey(key, false); - var existsInCache = await _hybridCache.ExistsAsync(cacheKey); + var existsInCache = await _hybridCache.ExistsAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (existsInCache is false) { uncachedKeys.Add(key); @@ -278,7 +278,7 @@ public async Task HasContentByIdAsync(int id, bool preview = false) return false; } - return await _hybridCache.ExistsAsync(GetCacheKey(keyAttempt.Result, preview)); + return await _hybridCache.ExistsAsync(GetCacheKey(keyAttempt.Result, preview), CancellationToken.None); } public async Task RefreshContentAsync(IContent content) diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs index 65b8f91945a5..46d782bdbecb 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs @@ -133,7 +133,7 @@ public async Task HasContentByIdAsync(int id) return false; } - return await _hybridCache.ExistsAsync($"{keyAttempt.Result}"); + return await _hybridCache.ExistsAsync($"{keyAttempt.Result}", CancellationToken.None); } public async Task RefreshMediaAsync(IMedia media) @@ -170,7 +170,7 @@ public async Task SeedAsync(CancellationToken cancellationToken) var cacheKey = GetCacheKey(key, false); - var existsInCache = await _hybridCache.ExistsAsync(cacheKey); + var existsInCache = await _hybridCache.ExistsAsync(cacheKey, CancellationToken.None); if (existsInCache is false) { uncachedKeys.Add(key); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs index 0c4207331a0e..088f14cd319a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -25,10 +26,10 @@ protected override void CustomTestSetup(IUmbracoBuilder builder) private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); - private IContentEditingService ContentEditingService => GetRequiredService(); - private IContentPublishingService ContentPublishingService => GetRequiredService(); + private IDocumentCacheService DocumentCacheService => GetRequiredService(); + private const string NewName = "New Name"; private const string NewTitle = "New Title"; @@ -460,6 +461,61 @@ public async Task Can_Not_Get_Deleted_Published_Content_By_Key(bool preview) Assert.IsNull(textPage); } + [Test] + public async Task Can_Get_Published_Content_By_Id_After_Previous_Check_Where_Not_Found() + { + // Arrange + var testPageKey = Guid.NewGuid(); + + // Act & Assert + // - assert we cannot get the content that doesn't yet exist from the cache + var testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey); + Assert.IsNull(testPage); + + testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey); + Assert.IsNull(testPage); + + // - create and publish the content + var testPageContent = ContentEditingBuilder.CreateBasicContent(ContentType.Key, testPageKey); + var createResult = await ContentEditingService.CreateAsync(testPageContent, Constants.Security.SuperUserKey); + Assert.IsTrue(createResult.Success); + var publishResult = await ContentPublishingService.PublishAsync(testPageKey, CultureAndSchedule, Constants.Security.SuperUserKey); + Assert.IsTrue(publishResult.Success); + + // - assert we can now get the content from the cache + testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey); + Assert.IsNotNull(testPage); + } + + [Test] + public async Task Can_Get_Published_Content_By_Id_After_Previous_Exists_Check() + { + // Act + var hasContentForTextPageCached = await DocumentCacheService.HasContentByIdAsync(PublishedTextPageId); + Assert.IsTrue(hasContentForTextPageCached); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId); + + // Assert + AssertPublishedTextPage(textPage); + } + + [Test] + public async Task Can_Do_Exists_Check_On_Created_Published_Content() + { + var testPageKey = Guid.NewGuid(); + var testPageContent = ContentEditingBuilder.CreateBasicContent(ContentType.Key, testPageKey); + var createResult = await ContentEditingService.CreateAsync(testPageContent, Constants.Security.SuperUserKey); + Assert.IsTrue(createResult.Success); + var publishResult = await ContentPublishingService.PublishAsync(testPageKey, CultureAndSchedule, Constants.Security.SuperUserKey); + Assert.IsTrue(publishResult.Success); + + var testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey); + Assert.IsNotNull(testPage); + + var hasContentForTextPageCached = await DocumentCacheService.HasContentByIdAsync(testPage.Id); + Assert.IsTrue(hasContentForTextPageCached); + } + private void AssertTextPage(IPublishedContent textPage) { Assert.Multiple(() => diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs index 152fe28b4ef6..8da30cd11883 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Caching.Hybrid; using Moq; using NUnit.Framework; @@ -33,15 +34,15 @@ public async Task ExistsAsync_WhenKeyExists_ShouldReturnTrue() _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(expectedValue); // Act - var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key); + var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsTrue(exists); @@ -56,24 +57,24 @@ public async Task ExistsAsync_WhenKeyDoesNotExist_ShouldReturnFalse() _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .Returns(( string key, - object? state, - Func> factory, + Func> state, + Func>, CancellationToken, ValueTask> factory, HybridCacheEntryOptions? options, IEnumerable? tags, CancellationToken token) => { - return factory(state!, token); + return factory(state, token); }); // Act - var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key); + var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsFalse(exists); @@ -89,15 +90,15 @@ public async Task TryGetValueAsync_WhenKeyExists_ShouldReturnTrueAndValueAsStrin _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(expectedValue); // Act - var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key); + var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsTrue(exists); @@ -114,15 +115,15 @@ public async Task TryGetValueAsync_WhenKeyExists_ShouldReturnTrueAndValueAsInteg _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(expectedValue); // Act - var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key); + var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsTrue(exists); @@ -138,15 +139,15 @@ public async Task TryGetValueAsync_WhenKeyExistsButValueIsNull_ShouldReturnTrueA _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(null!); // Act - var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key); + var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsTrue(exists); @@ -160,16 +161,16 @@ public async Task TryGetValueAsync_WhenKeyDoesNotExist_ShouldReturnFalseAndNull( string key = "test-key"; _cacheMock.Setup(cache => cache.GetOrCreateAsync( - key, - null, - It.IsAny>>(), - It.IsAny(), - null, - CancellationToken.None)) + key, + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), + It.IsAny(), + null, + CancellationToken.None)) .Returns(( string key, - object? state, - Func> factory, + Func> state, + Func>, CancellationToken, ValueTask> factory, HybridCacheEntryOptions? options, IEnumerable? tags, CancellationToken token) => @@ -178,7 +179,7 @@ public async Task TryGetValueAsync_WhenKeyDoesNotExist_ShouldReturnFalseAndNull( }); // Act - var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key); + var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsFalse(exists); From 1ceec183a3d28409b924b972a9bb88a360a9abd0 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 21 Oct 2025 11:38:01 +0200 Subject: [PATCH 055/155] Media: Fixes SQL error to ensure database relation between user group media start folder and deleted media item is removed (closes #20555) (#20572) Fixes SQL error to ensure database relation between user group media start folder and deleted media item is removed. --- .../Persistence/Repositories/Implement/MediaRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index 75dc8a3eb786..7dd53cbeb43e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -270,7 +270,7 @@ protected override IEnumerable GetDeleteClauses() "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2GranularPermission + " WHERE uniqueId IN (SELECT uniqueId FROM umbracoNode WHERE id = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", "UPDATE " + Constants.DatabaseSchema.Tables.UserGroup + - " SET startContentId = NULL WHERE startContentId = @id", + " SET startMediaId = NULL WHERE startMediaId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", From daace4b4a02d7a215fa23d00e5aebae37f352cc3 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 21 Oct 2025 12:05:10 +0200 Subject: [PATCH 056/155] Publishing: Resolve exceptions on publish branch (#20464) * Reduce log level of image cropper converter to avoid flooding logs with expected exceptions. * Don't run publish branch long running operation on a background thread such that UmbracoContext is available. * Revert to background thread and use EnsureUmbracoContext to ensure we can get an IUmbracoContext in the URL providers. * Updated tests. * Applied suggestion from code review. * Clarified comment. --- .../Services/ContentPublishingService.cs | 12 ++++++++++-- .../ValueConverters/ImageCropperValueConverter.cs | 7 ++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs index 698efbda917c..b46608aa14df 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Models.ContentPublishing; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Web; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -26,6 +27,7 @@ internal sealed class ContentPublishingService : IContentPublishingService private readonly IRelationService _relationService; private readonly ILogger _logger; private readonly ILongRunningOperationService _longRunningOperationService; + private readonly IUmbracoContextFactory _umbracoContextFactory; public ContentPublishingService( ICoreScopeProvider coreScopeProvider, @@ -37,7 +39,8 @@ public ContentPublishingService( IOptionsMonitor optionsMonitor, IRelationService relationService, ILogger logger, - ILongRunningOperationService longRunningOperationService) + ILongRunningOperationService longRunningOperationService, + IUmbracoContextFactory umbracoContextFactory) { _coreScopeProvider = coreScopeProvider; _contentService = contentService; @@ -53,6 +56,7 @@ public ContentPublishingService( { _contentSettings = contentSettings; }); + _umbracoContextFactory = umbracoContextFactory; } /// @@ -290,7 +294,7 @@ Attempt return MapInternalPublishingAttempt(minimalAttempt); } - _logger.LogInformation("Starting async background thread for publishing branch."); + _logger.LogDebug("Starting long running operation for publishing branch {Key} on background thread.", key); Attempt enqueueAttempt = await _longRunningOperationService.RunAsync( PublishBranchOperationType, async _ => await PerformPublishBranchAsync(key, cultures, publishBranchFilter, userKey, returnContent: false), @@ -324,6 +328,10 @@ private async Task(sourceString); } - catch (Exception ex) + catch (JsonException ex) { - // cannot deserialize, assume it may be a raw image URL - _logger.LogError(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString); + // Cannot deserialize, assume it may be a raw image URL. + _logger.LogDebug(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString); value = new ImageCropperValue { Src = sourceString }; } From 5488c77e0e960899c28f64fd51bffd8759a533cd Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 21 Oct 2025 15:26:29 +0200 Subject: [PATCH 057/155] Bumped version to 16.3.2. --- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 766ffa96baea..74bf09f5a9cf 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.3.1", + "version": "16.3.2", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index 922b55eca9ce..7167818c6d67 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": "16.3.1", + "version": "16.3.2", "assemblyVersion": { "precision": "build" }, From 8aa9dc8f1938aae457d4e22dcf5c96272b627057 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 21 Oct 2025 09:57:29 +0200 Subject: [PATCH 058/155] Hybrid Cache: Resolve start-up errors with mis-matched types (#20554) * Be consistent in use of GetOrCreateAsync overload in exists and retrieval. Ensure nullability of ContentCacheNode is consistent in exists and retrieval. * Applied suggestion from code review. * Move seeding to Umbraco application starting rather than started, ensuring an initial request is served. * Tighten up hybrid cache exists check with locking around check and remove, and use of cancellation token. --- .../UmbracoBuilderExtensions.cs | 3 +- .../Extensions/HybridCacheExtensions.cs | 67 +++++++++++++------ .../SeedingNotificationHandler.cs | 6 +- .../Services/DocumentCacheService.cs | 4 +- .../Services/MediaCacheService.cs | 4 +- .../DocumentHybridCacheTests.cs | 60 ++++++++++++++++- .../Extensions/HybridCacheExtensionsTests.cs | 55 +++++++-------- 7 files changed, 141 insertions(+), 58 deletions(-) diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs index ca625dacdf5c..e97c60fd6253 100644 --- a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,4 +1,3 @@ - using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -74,7 +73,7 @@ public static IUmbracoBuilder AddUmbracoHybridCache(this IUmbracoBuilder builder builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); builder.AddCacheSeeding(); return builder; } diff --git a/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs index ee1b7aefda05..ccd5897494eb 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Microsoft.Extensions.Caching.Hybrid; namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions; @@ -7,19 +8,24 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions; /// internal static class HybridCacheExtensions { + // Per-key semaphores to ensure the GetOrCreateAsync + RemoveAsync sequence + // executes atomically for a given cache key. + private static readonly ConcurrentDictionary _keyLocks = new(); + /// /// Returns true if the cache contains an item with a matching key. /// /// An instance of /// The name (key) of the item to search for in the cache. + /// The cancellation token. /// True if the item exists already. False if it doesn't. /// /// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191 /// Will never add or alter the state of any items in the cache. /// - public static async Task ExistsAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key) + public static async Task ExistsAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token) { - (bool exists, _) = await TryGetValueAsync(cache, key); + (bool exists, _) = await TryGetValueAsync(cache, key, token).ConfigureAwait(false); return exists; } @@ -29,34 +35,55 @@ public static async Task ExistsAsync(this Microsoft.Extensions.Caching. /// The type of the value of the item in the cache. /// An instance of /// The name (key) of the item to search for in the cache. + /// The cancellation token. /// A tuple of and the object (if found) retrieved from the cache. /// /// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191 /// Will never add or alter the state of any items in the cache. /// - public static async Task<(bool Exists, T? Value)> TryGetValueAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key) + public static async Task<(bool Exists, T? Value)> TryGetValueAsync(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token) { var exists = true; - T? result = await cache.GetOrCreateAsync( - key, - null!, - (_, _) => - { - exists = false; - return new ValueTask(default(T)!); - }, - new HybridCacheEntryOptions(), - null, - CancellationToken.None); - - // In checking for the existence of the item, if not found, we will have created a cache entry with a null value. - // So remove it again. - if (exists is false) + // Acquire a per-key semaphore so that GetOrCreateAsync and the possible RemoveAsync + // complete without another thread retrieving/creating the same key in-between. + SemaphoreSlim sem = _keyLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); + + await sem.WaitAsync().ConfigureAwait(false); + + try { - await cache.RemoveAsync(key); + T? result = await cache.GetOrCreateAsync( + key, + cancellationToken => + { + exists = false; + return default; + }, + new HybridCacheEntryOptions(), + null, + token).ConfigureAwait(false); + + // In checking for the existence of the item, if not found, we will have created a cache entry with a null value. + // So remove it again. Because we're holding the per-key lock there is no chance another thread + // will observe the temporary entry between GetOrCreateAsync and RemoveAsync. + if (exists is false) + { + await cache.RemoveAsync(key).ConfigureAwait(false); + } + + return (exists, result); } + finally + { + sem.Release(); - return (exists, result); + // Only remove the semaphore mapping if it still points to the same instance we used. + // This avoids removing another thread's semaphore or corrupting the map. + if (_keyLocks.TryGetValue(key, out SemaphoreSlim? current) && ReferenceEquals(current, sem)) + { + _keyLocks.TryRemove(key, out _); + } + } } } diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs index 0581bd26542c..38a6618c7019 100644 --- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; -internal sealed class SeedingNotificationHandler : INotificationAsyncHandler +internal sealed class SeedingNotificationHandler : INotificationAsyncHandler { private readonly IDocumentCacheService _documentCacheService; private readonly IMediaCacheService _mediaCacheService; @@ -24,7 +24,7 @@ public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IM _globalSettings = globalSettings.Value; } - public async Task HandleAsync(UmbracoApplicationStartedNotification notification, + public async Task HandleAsync(UmbracoApplicationStartingNotification notification, CancellationToken cancellationToken) { diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index 1675cb05cfb2..2879be5c8d7f 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -205,7 +205,7 @@ public async Task SeedAsync(CancellationToken cancellationToken) var cacheKey = GetCacheKey(key, false); - var existsInCache = await _hybridCache.ExistsAsync(cacheKey); + var existsInCache = await _hybridCache.ExistsAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (existsInCache is false) { uncachedKeys.Add(key); @@ -278,7 +278,7 @@ public async Task HasContentByIdAsync(int id, bool preview = false) return false; } - return await _hybridCache.ExistsAsync(GetCacheKey(keyAttempt.Result, preview)); + return await _hybridCache.ExistsAsync(GetCacheKey(keyAttempt.Result, preview), CancellationToken.None); } public async Task RefreshContentAsync(IContent content) diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs index 65b8f91945a5..46d782bdbecb 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs @@ -133,7 +133,7 @@ public async Task HasContentByIdAsync(int id) return false; } - return await _hybridCache.ExistsAsync($"{keyAttempt.Result}"); + return await _hybridCache.ExistsAsync($"{keyAttempt.Result}", CancellationToken.None); } public async Task RefreshMediaAsync(IMedia media) @@ -170,7 +170,7 @@ public async Task SeedAsync(CancellationToken cancellationToken) var cacheKey = GetCacheKey(key, false); - var existsInCache = await _hybridCache.ExistsAsync(cacheKey); + var existsInCache = await _hybridCache.ExistsAsync(cacheKey, CancellationToken.None); if (existsInCache is false) { uncachedKeys.Add(key); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs index 0c4207331a0e..088f14cd319a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -25,10 +26,10 @@ protected override void CustomTestSetup(IUmbracoBuilder builder) private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); - private IContentEditingService ContentEditingService => GetRequiredService(); - private IContentPublishingService ContentPublishingService => GetRequiredService(); + private IDocumentCacheService DocumentCacheService => GetRequiredService(); + private const string NewName = "New Name"; private const string NewTitle = "New Title"; @@ -460,6 +461,61 @@ public async Task Can_Not_Get_Deleted_Published_Content_By_Key(bool preview) Assert.IsNull(textPage); } + [Test] + public async Task Can_Get_Published_Content_By_Id_After_Previous_Check_Where_Not_Found() + { + // Arrange + var testPageKey = Guid.NewGuid(); + + // Act & Assert + // - assert we cannot get the content that doesn't yet exist from the cache + var testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey); + Assert.IsNull(testPage); + + testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey); + Assert.IsNull(testPage); + + // - create and publish the content + var testPageContent = ContentEditingBuilder.CreateBasicContent(ContentType.Key, testPageKey); + var createResult = await ContentEditingService.CreateAsync(testPageContent, Constants.Security.SuperUserKey); + Assert.IsTrue(createResult.Success); + var publishResult = await ContentPublishingService.PublishAsync(testPageKey, CultureAndSchedule, Constants.Security.SuperUserKey); + Assert.IsTrue(publishResult.Success); + + // - assert we can now get the content from the cache + testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey); + Assert.IsNotNull(testPage); + } + + [Test] + public async Task Can_Get_Published_Content_By_Id_After_Previous_Exists_Check() + { + // Act + var hasContentForTextPageCached = await DocumentCacheService.HasContentByIdAsync(PublishedTextPageId); + Assert.IsTrue(hasContentForTextPageCached); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId); + + // Assert + AssertPublishedTextPage(textPage); + } + + [Test] + public async Task Can_Do_Exists_Check_On_Created_Published_Content() + { + var testPageKey = Guid.NewGuid(); + var testPageContent = ContentEditingBuilder.CreateBasicContent(ContentType.Key, testPageKey); + var createResult = await ContentEditingService.CreateAsync(testPageContent, Constants.Security.SuperUserKey); + Assert.IsTrue(createResult.Success); + var publishResult = await ContentPublishingService.PublishAsync(testPageKey, CultureAndSchedule, Constants.Security.SuperUserKey); + Assert.IsTrue(publishResult.Success); + + var testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey); + Assert.IsNotNull(testPage); + + var hasContentForTextPageCached = await DocumentCacheService.HasContentByIdAsync(testPage.Id); + Assert.IsTrue(hasContentForTextPageCached); + } + private void AssertTextPage(IPublishedContent textPage) { Assert.Multiple(() => diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs index 152fe28b4ef6..8da30cd11883 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Caching.Hybrid; using Moq; using NUnit.Framework; @@ -33,15 +34,15 @@ public async Task ExistsAsync_WhenKeyExists_ShouldReturnTrue() _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(expectedValue); // Act - var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key); + var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsTrue(exists); @@ -56,24 +57,24 @@ public async Task ExistsAsync_WhenKeyDoesNotExist_ShouldReturnFalse() _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .Returns(( string key, - object? state, - Func> factory, + Func> state, + Func>, CancellationToken, ValueTask> factory, HybridCacheEntryOptions? options, IEnumerable? tags, CancellationToken token) => { - return factory(state!, token); + return factory(state, token); }); // Act - var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key); + var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsFalse(exists); @@ -89,15 +90,15 @@ public async Task TryGetValueAsync_WhenKeyExists_ShouldReturnTrueAndValueAsStrin _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(expectedValue); // Act - var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key); + var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsTrue(exists); @@ -114,15 +115,15 @@ public async Task TryGetValueAsync_WhenKeyExists_ShouldReturnTrueAndValueAsInteg _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(expectedValue); // Act - var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key); + var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsTrue(exists); @@ -138,15 +139,15 @@ public async Task TryGetValueAsync_WhenKeyExistsButValueIsNull_ShouldReturnTrueA _cacheMock .Setup(cache => cache.GetOrCreateAsync( key, - null!, - It.IsAny>>(), + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), It.IsAny(), null, CancellationToken.None)) .ReturnsAsync(null!); // Act - var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key); + var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsTrue(exists); @@ -160,16 +161,16 @@ public async Task TryGetValueAsync_WhenKeyDoesNotExist_ShouldReturnFalseAndNull( string key = "test-key"; _cacheMock.Setup(cache => cache.GetOrCreateAsync( - key, - null, - It.IsAny>>(), - It.IsAny(), - null, - CancellationToken.None)) + key, + It.IsAny>>(), + It.IsAny>, CancellationToken, ValueTask>>(), + It.IsAny(), + null, + CancellationToken.None)) .Returns(( string key, - object? state, - Func> factory, + Func> state, + Func>, CancellationToken, ValueTask> factory, HybridCacheEntryOptions? options, IEnumerable? tags, CancellationToken token) => @@ -178,7 +179,7 @@ public async Task TryGetValueAsync_WhenKeyDoesNotExist_ShouldReturnFalseAndNull( }); // Act - var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key); + var (exists, value) = await HybridCacheExtensions.TryGetValueAsync(_cacheMock.Object, key, CancellationToken.None); // Assert Assert.IsFalse(exists); From 942ccc82d9aca9aef02198dd75bdf6bf07c253d7 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:22:36 +0200 Subject: [PATCH 059/155] docs: Add 'Running Umbraco in Different Modes' section to copilot-instructions --- .github/copilot-instructions.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b438a0027b48..ed34279ab99b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -94,11 +94,18 @@ The solution contains 30 C# projects organized as follows: ## Common Tasks -### Frontend Development -For frontend-only changes: -1. Configure backend for frontend development: - ```json - +### Running Umbraco in Different Modes + +**Production Mode (Standard Development)** +Use this for backend development, testing full builds, or when you don't need hot reloading: +1. Build frontend assets: `cd src/Umbraco.Web.UI.Client && npm run build:for:cms` +2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build` +3. Access backoffice: `https://localhost:44339/umbraco` +4. Application uses compiled frontend from `wwwroot/umbraco/backoffice/` + +**Vite Dev Server Mode (Frontend Development with Hot Reload)** +Use this for frontend-only development with hot module reloading: +1. Configure backend for frontend development - Add to `src/Umbraco.Web.UI/appsettings.json` under `Umbraco:CMS:Security`: ```json "BackOfficeHost": "http://localhost:5173", "AuthorizeCallbackPathName": "/oauth_complete", @@ -107,6 +114,10 @@ For frontend-only changes: ``` 2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build` 3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server` +4. Access backoffice: `http://localhost:5173/` (no `/umbraco` prefix) +5. Changes to TypeScript/Lit files hot reload automatically + +**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode. ### Backend-Only Development For backend-only changes, disable frontend builds: From caeb3454e1fe37a45580dc3bf81a2a31fecab18e Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:22:57 +0200 Subject: [PATCH 060/155] build(dev): adds umbracoapplicationurl to vscode launch params --- .vscode/launch.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index ef4677989e20..f4d47c3dab86 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -101,6 +101,7 @@ "env": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:44339", + "UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL": "https://localhost:44339", "UMBRACO__CMS__SECURITY__BACKOFFICEHOST": "http://localhost:5173", "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKPATHNAME": "/oauth_complete", "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout", From 79639c0571ebd932d149ba0a1610281d04c39a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Oct 2025 11:52:09 +0200 Subject: [PATCH 061/155] Item Repository: Sort statuses by order of unique (#20603) * utility * ability to replace * deprecate removeStatus * no need to call this any longer * Sort statuses and ensure not appending statuses, only updating them # Conflicts: # src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts --- .../observable-api/states/array-state.test.ts | 70 ++++++++++++------- .../libs/observable-api/states/array-state.ts | 33 +++++++++ .../src/libs/observable-api/utils/index.ts | 1 + .../utils/replace-in-unique-array.function.ts | 19 +++++ .../core/picker-input/picker-input.context.ts | 1 - .../repository/repository-items.manager.ts | 34 ++++++--- 6 files changed, 119 insertions(+), 39 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/replace-in-unique-array.function.ts diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts index d9d5d1ad4f88..74c2e63c5ccc 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.test.ts @@ -5,7 +5,7 @@ describe('ArrayState', () => { type ObjectType = { key: string; another: string }; type ArrayType = ObjectType[]; - let subject: UmbArrayState; + let state: UmbArrayState; let initialData: ArrayType; beforeEach(() => { @@ -14,12 +14,12 @@ describe('ArrayState', () => { { key: '2', another: 'myValue2' }, { key: '3', another: 'myValue3' }, ]; - subject = new UmbArrayState(initialData, (x) => x.key); + state = new UmbArrayState(initialData, (x) => x.key); }); it('replays latests, no matter the amount of subscriptions.', (done) => { let amountOfCallbacks = 0; - const observer = subject.asObservable(); + const observer = state.asObservable(); observer.subscribe((value) => { amountOfCallbacks++; expect(value).to.be.equal(initialData); @@ -36,8 +36,8 @@ describe('ArrayState', () => { it('remove method, removes the one with the key', (done) => { const expectedData = [initialData[0], initialData[2]]; - subject.remove(['2']); - const observer = subject.asObservable(); + state.remove(['2']); + const observer = state.asObservable(); observer.subscribe((value) => { expect(JSON.stringify(value)).to.be.equal(JSON.stringify(expectedData)); done(); @@ -45,17 +45,17 @@ describe('ArrayState', () => { }); it('getHasOne method, return true when key exists', () => { - expect(subject.getHasOne('2')).to.be.true; + expect(state.getHasOne('2')).to.be.true; }); it('getHasOne method, return false when key does not exists', () => { - expect(subject.getHasOne('1337')).to.be.false; + expect(state.getHasOne('1337')).to.be.false; }); it('filter method, removes anything that is not true of the given predicate method', (done) => { const expectedData = [initialData[0], initialData[2]]; - subject.filter((x) => x.key !== '2'); - const observer = subject.asObservable(); + state.filter((x) => x.key !== '2'); + const observer = state.asObservable(); observer.subscribe((value) => { expect(JSON.stringify(value)).to.be.equal(JSON.stringify(expectedData)); done(); @@ -64,11 +64,11 @@ describe('ArrayState', () => { it('add new item via appendOne method.', (done) => { const newItem = { key: '4', another: 'myValue4' }; - subject.appendOne(newItem); + state.appendOne(newItem); const expectedData = [...initialData, newItem]; - const observer = subject.asObservable(); + const observer = state.asObservable(); observer.subscribe((value) => { expect(value.length).to.be.equal(expectedData.length); expect(value[3].another).to.be.equal(expectedData[3].another); @@ -78,9 +78,25 @@ describe('ArrayState', () => { it('partially update an existing item via updateOne method.', (done) => { const newItem = { another: 'myValue2.2' }; - subject.updateOne('2', newItem); + state.updateOne('2', newItem); - const observer = subject.asObservable(); + const observer = state.asObservable(); + observer.subscribe((value) => { + expect(value.length).to.be.equal(initialData.length); + expect(value[0].another).to.be.equal('myValue1'); + expect(value[1].another).to.be.equal('myValue2.2'); + done(); + }); + }); + + it('replaces only existing items via replace method.', (done) => { + const newItems = [ + { key: '2', another: 'myValue2.2' }, + { key: '4', another: 'myValue4.4' }, + ]; + state.replace(newItems); + + const observer = state.asObservable(); observer.subscribe((value) => { expect(value.length).to.be.equal(initialData.length); expect(value[0].another).to.be.equal('myValue1'); @@ -90,7 +106,7 @@ describe('ArrayState', () => { }); it('getObservablePart for a specific entry of array', (done) => { - const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === '2')); + const subObserver = state.asObservablePart((data) => data.find((x) => x.key === '2')); subObserver.subscribe((entry) => { if (entry) { expect(entry.another).to.be.equal(initialData[1].another); @@ -103,7 +119,7 @@ describe('ArrayState', () => { let amountOfCallbacks = 0; const newItem = { key: '4', another: 'myValue4' }; - const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === newItem.key)); + const subObserver = state.asObservablePart((data) => data.find((x) => x.key === newItem.key)); subObserver.subscribe((entry) => { amountOfCallbacks++; if (amountOfCallbacks === 1) { @@ -118,16 +134,16 @@ describe('ArrayState', () => { } }); - subject.appendOne(newItem); + state.appendOne(newItem); }); it('asObservable returns the replaced item', (done) => { const newItem = { key: '2', another: 'myValue4' }; - subject.appendOne(newItem); + state.appendOne(newItem); const expectedData = [initialData[0], newItem, initialData[2]]; - const observer = subject.asObservable(); + const observer = state.asObservable(); observer.subscribe((value) => { expect(value.length).to.be.equal(expectedData.length); expect(value[1].another).to.be.equal(newItem.another); @@ -137,9 +153,9 @@ describe('ArrayState', () => { it('getObservablePart returns the replaced item', (done) => { const newItem = { key: '2', another: 'myValue4' }; - subject.appendOne(newItem); + state.appendOne(newItem); - const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === newItem.key)); + const subObserver = state.asObservablePart((data) => data.find((x) => x.key === newItem.key)); subObserver.subscribe((entry) => { expect(entry).to.be.equal(newItem); // Second callback should give us the right data: if (entry) { @@ -152,7 +168,7 @@ describe('ArrayState', () => { it('getObservablePart replays existing data to any amount of subscribers.', (done) => { let amountOfCallbacks = 0; - const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === '2')); + const subObserver = state.asObservablePart((data) => data.find((x) => x.key === '2')); subObserver.subscribe((entry) => { if (entry) { amountOfCallbacks++; @@ -173,7 +189,7 @@ describe('ArrayState', () => { it('getObservablePart replays existing data to any amount of subscribers.', (done) => { let amountOfCallbacks = 0; - const subObserver = subject.asObservablePart((data) => data.find((x) => x.key === '2')); + const subObserver = state.asObservablePart((data) => data.find((x) => x.key === '2')); subObserver.subscribe((entry) => { if (entry) { amountOfCallbacks++; @@ -194,7 +210,7 @@ describe('ArrayState', () => { it('append only updates observable if changes item', (done) => { let count = 0; - const observer = subject.asObservable(); + const observer = state.asObservable(); observer.subscribe((value) => { count++; if (count === 1) { @@ -212,12 +228,12 @@ describe('ArrayState', () => { Promise.resolve().then(() => { // Despite how many times this happens it should not trigger any change. - subject.append(initialData); - subject.append(initialData); - subject.append(initialData); + state.append(initialData); + state.append(initialData); + state.append(initialData); Promise.resolve().then(() => { - subject.appendOne({ key: '4', another: 'myValue4' }); + state.appendOne({ key: '4', another: 'myValue4' }); }); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts index 26cf9261abf2..3dab6a21c062 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts @@ -1,6 +1,7 @@ import { partialUpdateFrozenArray } from '../utils/partial-update-frozen-array.function.js'; import { pushAtToUniqueArray } from '../utils/push-at-to-unique-array.function.js'; import { pushToUniqueArray } from '../utils/push-to-unique-array.function.js'; +import { replaceInUniqueArray } from '../utils/replace-in-unique-array.function.js'; import { UmbDeepState } from './deep-state.js'; /** @@ -262,6 +263,38 @@ export class UmbArrayState extends UmbDeepState { return this; } + /** + * @function replace + * @param {Partial} entires - data of entries to be replaced. + * @returns {UmbArrayState} Reference to it self. + * @description - Replaces one or more entries, requires the ArrayState to be constructed with a getUnique method. + * @example Example append some data. + * const data = [ + * { key: 1, value: 'foo'}, + * { key: 2, value: 'bar'} + * ]; + * const myState = new UmbArrayState(data, (x) => x.key); + * const updates = [ + * { key: 1, value: 'foo2'}, + * { key: 3, value: 'bar2'} + * ]; + * myState.replace(updates); + * // Only the existing item gets replaced: + * myState.getValue(); // -> [{ key: 1, value: 'foo2'}, { key: 2, value: 'bar'}] + */ + replace(entries: Array): UmbArrayState { + if (this.getUniqueMethod) { + const next = [...this.getValue()]; + entries.forEach((entry) => { + replaceInUniqueArray(next, entry as T, this.getUniqueMethod!); + }); + this.setValue(next); + } else { + throw new Error("Can't replace entries of an ArrayState without a getUnique method provided when constructed."); + } + return this; + } + /** * @function updateOne * @param {U} unique - Unique value to find entry to update. diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/index.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/index.ts index 3b3452ab110e..0c90285e6c80 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/index.ts @@ -12,5 +12,6 @@ export * from './observe-multiple.function.js'; export * from './partial-update-frozen-array.function.js'; export * from './push-at-to-unique-array.function.js'; export * from './push-to-unique-array.function.js'; +export * from './replace-in-unique-array.function.js'; export * from './simple-hash-code.function.js'; export * from './strict-equality-memoization.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/replace-in-unique-array.function.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/replace-in-unique-array.function.ts new file mode 100644 index 000000000000..1970c0b2a9e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/replace-in-unique-array.function.ts @@ -0,0 +1,19 @@ +/** + * @function replaceInUniqueArray + * @param {T[]} data - An array of objects. + * @param {T} entry - The object to replace with. + * @param {getUniqueMethod: (entry: T) => unknown} [getUniqueMethod] - Method to get the unique value of an entry. + * @description - Replaces an item of an Array. + * @example Example replace an entry of an Array. Where the key is unique and the item will only be replaced if matched with existing. + * const data = [{key: 'myKey', value:'initialValue'}]; + * const entry = {key: 'myKey', value: 'replacedValue'}; + * const newDataSet = replaceInUniqueArray(data, entry, x => x.key === key); + */ +export function replaceInUniqueArray(data: T[], entry: T, getUniqueMethod: (entry: T) => unknown): T[] { + const unique = getUniqueMethod(entry); + const indexToReplace = data.findIndex((x) => getUniqueMethod(x) === unique); + if (indexToReplace !== -1) { + data[indexToReplace] = entry; + } + return data; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index de943aa88aa8..c345fd22d4ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -133,7 +133,6 @@ export class UmbPickerInputContext< #removeItem(unique: string) { const newSelection = this.getSelection().filter((value) => value !== unique); this.setSelection(newSelection); - this.#itemManager.removeStatus(unique); this.getHostElement().dispatchEvent(new UmbChangeEvent()); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts index cf227b3cf28b..c60dc8badf25 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts @@ -75,17 +75,20 @@ export class UmbRepositoryItemsManager exte (uniques) => { if (uniques.length === 0) { this.#items.setValue([]); + this.#statuses.setValue([]); return; } // TODO: This could be optimized so we only load the appended items, but this requires that the response checks that an item is still present in uniques. [NL] - // Check if we already have the items, and then just sort them: - const items = this.#items.getValue(); + // Check if we already have the statuses, and then just sort them: + const statuses = this.#statuses.getValue(); if ( - uniques.length === items.length && - uniques.every((unique) => items.find((item) => this.#getUnique(item) === unique)) + uniques.length === statuses.length && + uniques.every((unique) => statuses.find((status) => status.unique === unique)) ) { + const items = this.#items.getValue(); this.#items.setValue(this.#sortByUniques(items)); + this.#statuses.setValue(this.#sortByUniques(statuses)); } else { // We need to load new items, so ... this.#requestItems(); @@ -124,9 +127,17 @@ export class UmbRepositoryItemsManager exte return this.#items.asObservablePart((items) => items.find((item) => this.#getUnique(item) === unique)); } + /** + * @deprecated - This is resolved by setUniques, no need to update statuses. + * @param unique {string} - The unique identifier of the item to remove the status of. + */ removeStatus(unique: string) { - const newStatuses = this.#statuses.getValue().filter((status) => status.unique !== unique); - this.#statuses.setValue(newStatuses); + new UmbDeprecation({ + removeInVersion: '18.0.0', + deprecated: 'removeStatus', + solution: 'Statuses are removed automatically when setting uniques', + }).warn(); + this.#statuses.filter((status) => status.unique !== unique); } async getItemByUnique(unique: string) { @@ -144,6 +155,7 @@ export class UmbRepositoryItemsManager exte const requestedUniques = this.getUniques(); this.#statuses.setValue( + // No need to do sorting here as we just got the unique in the right order above. requestedUniques.map((unique) => ({ state: { type: 'loading', @@ -164,7 +176,7 @@ export class UmbRepositoryItemsManager exte } if (error) { - this.#statuses.append( + this.#statuses.replace( requestedUniques.map((unique) => ({ state: { type: 'error', @@ -185,7 +197,7 @@ export class UmbRepositoryItemsManager exte const resolvedUniques = requestedUniques.filter((unique) => !rejectedUniques.includes(unique)); this.#items.remove(rejectedUniques); - this.#statuses.append([ + this.#statuses.replace([ ...rejectedUniques.map( (unique) => ({ @@ -226,12 +238,11 @@ export class UmbRepositoryItemsManager exte const { data, error } = await this.repository.requestItems([unique]); if (error) { - this.#statuses.appendOne({ + this.#statuses.updateOne(unique, { state: { type: 'error', error: '#general_notFound', }, - unique, } as UmbRepositoryItemsStatus); } @@ -244,11 +255,12 @@ export class UmbRepositoryItemsManager exte const newItems = [...items]; newItems[index] = data[0]; this.#items.setValue(this.#sortByUniques(newItems)); + // No need to update statuses here, as the item is the same, just updated. } } } - #sortByUniques(data?: Array): Array { + #sortByUniques>(data?: Array): Array { if (!data) return []; const uniques = this.getUniques(); return [...data].sort((a, b) => { From 48759b9852990f021fb4cc0ffb5609c91ba79061 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 22 Oct 2025 12:21:42 +0200 Subject: [PATCH 062/155] Migrations: Use reliable GUID to check for existence of data type when creating (#20604) * Use reliable GUID to check for existence of data type in migration. * Retrieve just a single field in existence check. --- .../MigrateMediaTypeLabelProperties.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs index efa48f00f276..71c824f35758 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs @@ -6,7 +6,9 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_3_0; @@ -69,7 +71,7 @@ private void ToggleIndentityInsertForNodes(bool toggleOn) private void IfNotExistsCreateBytesLabel() { - if (Database.Exists(Constants.DataTypes.LabelBytes)) + if (NodeExists(_labelBytesDataTypeKey)) { return; } @@ -89,7 +91,7 @@ private void IfNotExistsCreateBytesLabel() CreateDate = DateTime.Now, }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); var dataTypeDto = new DataTypeDto { @@ -100,12 +102,12 @@ private void IfNotExistsCreateBytesLabel() Configuration = "{\"umbracoDataValueType\":\"BIGINT\", \"labelTemplate\":\"{=value | bytes}\"}", }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); } private void IfNotExistsCreatePixelsLabel() { - if (Database.Exists(Constants.DataTypes.LabelPixels)) + if (NodeExists(_labelPixelsDataTypeKey)) { return; } @@ -125,7 +127,7 @@ private void IfNotExistsCreatePixelsLabel() CreateDate = DateTime.Now, }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); var dataTypeDto = new DataTypeDto { @@ -136,7 +138,16 @@ private void IfNotExistsCreatePixelsLabel() Configuration = "{\"umbracoDataValueType\":\"INT\", \"labelTemplate\":\"{=value}px\"}", }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + + private bool NodeExists(Guid uniqueId) + { + Sql sql = Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.UniqueId == uniqueId); + return Database.FirstOrDefault(sql) is not null; } private async Task MigrateMediaTypeLabels() From 21bf23b67df097cd0d40c8a44e582055185ff973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Oct 2025 12:37:13 +0200 Subject: [PATCH 063/155] Dictionary: Fix shortcut Ctrl + S not saving dictionary items (#20605) * switched event listener from 'change' to 'input' * Update workspace-view-dictionary-editor.element.ts --- .../workspace-view-dictionary-editor.element.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts index b987fc3a64f6..dff606e5197f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts @@ -1,7 +1,6 @@ import { UMB_DICTIONARY_WORKSPACE_CONTEXT } from '../dictionary-workspace.context-token.js'; import type { UmbDictionaryDetailModel } from '../../types.js'; import type { UUITextareaElement } from '@umbraco-cms/backoffice/external/uui'; -import { UUITextareaEvent } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; @@ -72,13 +71,11 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { } #onTextareaChange(e: Event) { - if (e instanceof UUITextareaEvent) { - const target = e.composedPath()[0] as UUITextareaElement; - const translation = (target.value as string).toString(); - const isoCode = target.getAttribute('name')!; + const target = e.composedPath()[0] as UUITextareaElement; + const translation = (target.value as string).toString(); + const isoCode = target.getAttribute('name')!; - this.#workspaceContext?.setPropertyValue(isoCode, translation); - } + this.#workspaceContext?.setPropertyValue(isoCode, translation); } override render() { @@ -104,7 +101,7 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { slot="editor" name=${language.unique} label="translation" - @change=${this.#onTextareaChange} + @input=${this.#onTextareaChange} .value=${translation?.translation ?? ''} ?readonly=${this.#isReadOnly(language.unique)}> `; From c2eea5d6cc95e328d2b55831a5b6683c3bfdc396 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 22 Oct 2025 13:37:19 +0200 Subject: [PATCH 064/155] Populate IncludeDescendants on ContentPublishedNotification when publishing branch (forward port of #20578). --- src/Umbraco.Core/Notifications/ContentPublishedNotification.cs | 1 + src/Umbraco.Core/Services/ContentService.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs b/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs index 07baedc47301..f242ef20a95a 100644 --- a/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs @@ -24,6 +24,7 @@ public ContentPublishedNotification(IEnumerable target, EventMessages public ContentPublishedNotification(IEnumerable target, EventMessages messages, bool includeDescendants) : base(target, messages) => IncludeDescendants = includeDescendants; + /// /// Gets a enumeration of which are being published. /// diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 5d4ed2f98ca8..fd12d4dbb53e 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -2207,7 +2207,7 @@ internal IEnumerable PublishBranch( variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"], null, eventMessages)); - scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState)); + scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(notificationState)); scope.Complete(); } From 62c1d44a5d924085d526426d45722e1c61c84387 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Wed, 22 Oct 2025 13:46:56 +0200 Subject: [PATCH 065/155] Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled (#20559) * Register slimmed down OutputExpansionStrategy for webhooks if deliveryapi is not enabled * PR review comment resolution --- ...tContextOutputExpansionStrategyAccessor.cs | 12 ++ .../RequestContextServiceAccessorBase.cs | 20 +++ .../ElementOnlyOutputExpansionStrategy.cs | 148 ++++++++++++++++++ .../UmbracoBuilderExtensions.cs | 33 ++-- ...RequestContextOutputExpansionStrategyV2.cs | 145 +---------------- .../WebhooksBuilderExtensions.cs | 10 +- 6 files changed, 214 insertions(+), 154 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Common/Accessors/RequestContextOutputExpansionStrategyAccessor.cs create mode 100644 src/Umbraco.Cms.Api.Common/Accessors/RequestContextServiceAccessorBase.cs create mode 100644 src/Umbraco.Cms.Api.Common/Rendering/ElementOnlyOutputExpansionStrategy.cs diff --git a/src/Umbraco.Cms.Api.Common/Accessors/RequestContextOutputExpansionStrategyAccessor.cs b/src/Umbraco.Cms.Api.Common/Accessors/RequestContextOutputExpansionStrategyAccessor.cs new file mode 100644 index 000000000000..cb375857b792 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Accessors/RequestContextOutputExpansionStrategyAccessor.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core.DeliveryApi; + +namespace Umbraco.Cms.Api.Common.Accessors; + +public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase, IOutputExpansionStrategyAccessor +{ + public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor) + : base(httpContextAccessor) + { + } +} diff --git a/src/Umbraco.Cms.Api.Common/Accessors/RequestContextServiceAccessorBase.cs b/src/Umbraco.Cms.Api.Common/Accessors/RequestContextServiceAccessorBase.cs new file mode 100644 index 000000000000..2748746a964d --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Accessors/RequestContextServiceAccessorBase.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Umbraco.Cms.Api.Common.Accessors; + +public abstract class RequestContextServiceAccessorBase + where T : class +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor) + => _httpContextAccessor = httpContextAccessor; + + public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService) + { + requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService(); + return requestStartNodeService is not null; + } +} diff --git a/src/Umbraco.Cms.Api.Common/Rendering/ElementOnlyOutputExpansionStrategy.cs b/src/Umbraco.Cms.Api.Common/Rendering/ElementOnlyOutputExpansionStrategy.cs new file mode 100644 index 000000000000..a5f113c7c760 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Rendering/ElementOnlyOutputExpansionStrategy.cs @@ -0,0 +1,148 @@ +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Common.Rendering; + +public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy +{ + protected const string All = "$all"; + protected const string None = ""; + protected const string ExpandParameterName = "expand"; + protected const string FieldsParameterName = "fields"; + + private readonly IApiPropertyRenderer _propertyRenderer; + + protected Stack ExpandProperties { get; } = new(); + + protected Stack IncludeProperties { get; } = new(); + + public ElementOnlyOutputExpansionStrategy( + IApiPropertyRenderer propertyRenderer) + { + _propertyRenderer = propertyRenderer; + } + + public virtual IDictionary MapContentProperties(IPublishedContent content) + => content.ItemType == PublishedItemType.Content + ? MapProperties(content.Properties) + : throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}"); + + public virtual IDictionary MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true) + { + if (media.ItemType != PublishedItemType.Media) + { + throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}"); + } + + IPublishedProperty[] properties = media + .Properties + .Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false) + .ToArray(); + + return properties.Any() + ? MapProperties(properties) + : new Dictionary(); + } + + public virtual IDictionary MapElementProperties(IPublishedElement element) + => MapProperties(element.Properties, true); + + private IDictionary MapProperties(IEnumerable properties, bool forceExpandProperties = false) + { + Node? currentExpandProperties = ExpandProperties.Count > 0 ? ExpandProperties.Peek() : null; + if (ExpandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false) + { + return new Dictionary(); + } + + Node? currentIncludeProperties = IncludeProperties.Count > 0 ? IncludeProperties.Peek() : null; + var result = new Dictionary(); + foreach (IPublishedProperty property in properties) + { + Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias); + if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null) + { + continue; + } + + Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias); + + IncludeProperties.Push(nextIncludeProperties); + ExpandProperties.Push(nextExpandProperties); + + result[property.Alias] = GetPropertyValue(property); + + ExpandProperties.Pop(); + IncludeProperties.Pop(); + } + + return result; + } + + private Node? GetNextProperties(Node? currentProperties, string propertyAlias) + => currentProperties?.Items.FirstOrDefault(i => i.Key == All) + ?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias); + + private object? GetPropertyValue(IPublishedProperty property) + => _propertyRenderer.GetPropertyValue(property, ExpandProperties.Peek() is not null); + + protected sealed class Node + { + public string Key { get; private set; } = string.Empty; + + public List Items { get; } = new(); + + public static Node Parse(string value) + { + // verify that there are as many start brackets as there are end brackets + if (value.CountOccurrences("[") != value.CountOccurrences("]")) + { + throw new ArgumentException("Value did not contain an equal number of start and end brackets"); + } + + // verify that the value does not start with a start bracket + if (value.StartsWith("[")) + { + throw new ArgumentException("Value cannot start with a bracket"); + } + + // verify that there are no empty brackets + if (value.Contains("[]")) + { + throw new ArgumentException("Value cannot contain empty brackets"); + } + + var stack = new Stack(); + var root = new Node { Key = "root" }; + stack.Push(root); + + var currentNode = new Node(); + root.Items.Add(currentNode); + + foreach (char c in value) + { + switch (c) + { + case '[': // Start a new node, child of the current node + stack.Push(currentNode); + currentNode = new Node(); + stack.Peek().Items.Add(currentNode); + break; + case ',': // Start a new node, but at the same level of the current node + currentNode = new Node(); + stack.Peek().Items.Add(currentNode); + break; + case ']': // Back to parent of the current node + currentNode = stack.Pop(); + break; + default: // Add char to current node key + currentNode.Key += c; + break; + } + } + + return root; + } + } +} diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs index 7d200398977c..a0860a342234 100644 --- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs @@ -35,28 +35,35 @@ public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddScoped(provider => - { - HttpContext? httpContext = provider.GetRequiredService().HttpContext; - ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion(); - if (apiVersion is null) + + builder.Services.AddUnique( + provider => { - return provider.GetRequiredService(); - } + HttpContext? httpContext = provider.GetRequiredService().HttpContext; + ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion(); + if (apiVersion is null) + { + return provider.GetRequiredService(); + } + + // V1 of the Delivery API uses a different expansion strategy than V2+ + return apiVersion.MajorVersion == 1 + ? provider.GetRequiredService() + : provider.GetRequiredService(); + }, + ServiceLifetime.Scoped); - // V1 of the Delivery API uses a different expansion strategy than V2+ - return apiVersion.MajorVersion == 1 - ? provider.GetRequiredService() - : provider.GetRequiredService(); - }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + + // Webooks register a more basic implementation, remove it. + builder.Services.AddUnique(ServiceLifetime.Singleton); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs index e1a29b3ec718..779ed31083fe 100644 --- a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs +++ b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs @@ -1,62 +1,25 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Api.Common.Rendering; using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Rendering; -internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy +internal sealed class RequestContextOutputExpansionStrategyV2 : ElementOnlyOutputExpansionStrategy, IOutputExpansionStrategy { - private const string All = "$all"; - private const string None = ""; - private const string ExpandParameterName = "expand"; - private const string FieldsParameterName = "fields"; - - private readonly IApiPropertyRenderer _propertyRenderer; private readonly ILogger _logger; - private readonly Stack _expandProperties; - private readonly Stack _includeProperties; - public RequestContextOutputExpansionStrategyV2( IHttpContextAccessor httpContextAccessor, IApiPropertyRenderer propertyRenderer, ILogger logger) + : base(propertyRenderer) { - _propertyRenderer = propertyRenderer; _logger = logger; - _expandProperties = new Stack(); - _includeProperties = new Stack(); InitializeExpandAndInclude(httpContextAccessor); } - public IDictionary MapContentProperties(IPublishedContent content) - => content.ItemType == PublishedItemType.Content - ? MapProperties(content.Properties) - : throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}"); - - public IDictionary MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true) - { - if (media.ItemType != PublishedItemType.Media) - { - throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}"); - } - - IPublishedProperty[] properties = media - .Properties - .Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false) - .ToArray(); - - return properties.Any() - ? MapProperties(properties) - : new Dictionary(); - } - - public IDictionary MapElementProperties(IPublishedElement element) - => MapProperties(element.Properties, true); - private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor) { string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key]; @@ -66,7 +29,7 @@ private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor try { - _expandProperties.Push(Node.Parse(toExpand)); + ExpandProperties.Push(Node.Parse(toExpand)); } catch (ArgumentException ex) { @@ -76,7 +39,7 @@ private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor try { - _includeProperties.Push(Node.Parse(toInclude)); + IncludeProperties.Push(Node.Parse(toInclude)); } catch (ArgumentException ex) { @@ -84,102 +47,4 @@ private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}"); } } - - private IDictionary MapProperties(IEnumerable properties, bool forceExpandProperties = false) - { - Node? currentExpandProperties = _expandProperties.Peek(); - if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false) - { - return new Dictionary(); - } - - Node? currentIncludeProperties = _includeProperties.Peek(); - var result = new Dictionary(); - foreach (IPublishedProperty property in properties) - { - Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias); - if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null) - { - continue; - } - - Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias); - - _includeProperties.Push(nextIncludeProperties); - _expandProperties.Push(nextExpandProperties); - - result[property.Alias] = GetPropertyValue(property); - - _expandProperties.Pop(); - _includeProperties.Pop(); - } - - return result; - } - - private Node? GetNextProperties(Node? currentProperties, string propertyAlias) - => currentProperties?.Items.FirstOrDefault(i => i.Key == All) - ?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias); - - private object? GetPropertyValue(IPublishedProperty property) - => _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null); - - private sealed class Node - { - public string Key { get; private set; } = string.Empty; - - public List Items { get; } = new(); - - public static Node Parse(string value) - { - // verify that there are as many start brackets as there are end brackets - if (value.CountOccurrences("[") != value.CountOccurrences("]")) - { - throw new ArgumentException("Value did not contain an equal number of start and end brackets"); - } - - // verify that the value does not start with a start bracket - if (value.StartsWith("[")) - { - throw new ArgumentException("Value cannot start with a bracket"); - } - - // verify that there are no empty brackets - if (value.Contains("[]")) - { - throw new ArgumentException("Value cannot contain empty brackets"); - } - - var stack = new Stack(); - var root = new Node { Key = "root" }; - stack.Push(root); - - var currentNode = new Node(); - root.Items.Add(currentNode); - - foreach (char c in value) - { - switch (c) - { - case '[': // Start a new node, child of the current node - stack.Push(currentNode); - currentNode = new Node(); - stack.Peek().Items.Add(currentNode); - break; - case ',': // Start a new node, but at the same level of the current node - currentNode = new Node(); - stack.Peek().Items.Add(currentNode); - break; - case ']': // Back to parent of the current node - currentNode = stack.Pop(); - break; - default: // Add char to current node key - currentNode.Key += c; - break; - } - } - - return root; - } - } } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs index 8d2d20a1d65d..81d764f69793 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs @@ -1,5 +1,9 @@ -using Umbraco.Cms.Api.Management.Factories; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.Accessors; +using Umbraco.Cms.Api.Common.Rendering; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Mapping.Webhook; +using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Extensions; @@ -12,6 +16,10 @@ internal static IUmbracoBuilder AddWebhooks(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.AddMapDefinition(); + // deliveryApi will overwrite these more basic ones. + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + return builder; } } From 4a65f56d9d10d344ed117b209b444e6790403287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Oct 2025 13:57:09 +0200 Subject: [PATCH 066/155] =?UTF-8?q?Hotfix:=20Implement=20a=20specific=20so?= =?UTF-8?q?rting=20method=20for=20statuses=20as=20the=20existing=20has=20?= =?UTF-8?q?=E2=80=A6=20(#20609)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a specific sorting method for statuses as the existing has to support deprecated implementation of custom getUnique method --- .../core/repository/repository-items.manager.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts index c60dc8badf25..0b50d72399ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts @@ -88,7 +88,7 @@ export class UmbRepositoryItemsManager exte ) { const items = this.#items.getValue(); this.#items.setValue(this.#sortByUniques(items)); - this.#statuses.setValue(this.#sortByUniques(statuses)); + this.#statuses.setValue(this.#sortStatusByUniques(statuses)); } else { // We need to load new items, so ... this.#requestItems(); @@ -260,7 +260,7 @@ export class UmbRepositoryItemsManager exte } } - #sortByUniques>(data?: Array): Array { + #sortByUniques(data?: Array): Array { if (!data) return []; const uniques = this.getUniques(); return [...data].sort((a, b) => { @@ -270,6 +270,17 @@ export class UmbRepositoryItemsManager exte }); } + /** Just needed for the deprecation implementation to work, do not bring this into 17.0 [NL] */ + #sortStatusByUniques(data?: Array): Array { + if (!data) return []; + const uniques = this.getUniques(); + return [...data].sort((a, b) => { + const aIndex = uniques.indexOf(a.unique ?? ''); + const bIndex = uniques.indexOf(b.unique ?? ''); + return aIndex - bIndex; + }); + } + #onEntityUpdatedEvent = (event: UmbEntityUpdatedEvent) => { const eventUnique = event.getUnique(); From 194fee7c916a7632e2c8f0249112ca6cb6089189 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 22 Oct 2025 12:54:47 +0200 Subject: [PATCH 067/155] Use tryExecute for delete API call Replaces direct await of #delete with tryExecute to improve error handling in the delete method of UmbManagementApiDetailDataRequestManager. --- .../management-api/detail/detail-data.request-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts index cf33e0c4f5cf..db0507475a84 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts @@ -125,7 +125,7 @@ export class UmbManagementApiDetailDataRequestManager< } async delete(id: string): Promise { - const { error } = await this.#delete(id); + const { error } = await tryExecute(this, this.#delete(id)); // Only update the cache when we are connected to the server events if (this.#isConnectedToServerEvents && !error) { From f88e28d642b90fcc78b258be1a1c98ea4a588cc9 Mon Sep 17 00:00:00 2001 From: Nicklas Kramer Date: Wed, 22 Oct 2025 15:12:39 +0200 Subject: [PATCH 068/155] Filesystem: Prevent tree showing other filetypes than the supported ones (#20567) * Added check to only find .css files in FileSystemTreeServiceBase.cs * Marking GetFiles as virtual and overriding it in StyleSheetTreeService.cs to only find .css files * Redone tests to fit new format * Fix tests to use file extensions * Adding file extensions to all other relevant tests * Adding file filter to remaining trees * Adding tests to ensure invalid filetypes wont show * Encapulation and resolved minor warnings in tests. --------- Co-authored-by: Andy Butland --- .../FileSystem/FileSystemTreeServiceBase.cs | 3 ++ .../FileSystem/PartialViewTreeService.cs | 2 ++ .../Services/FileSystem/ScriptTreeService.cs | 2 ++ .../FileSystem/StyleSheetTreeService.cs | 4 ++- .../Trees/FileSystemTreeServiceTestsBase.cs | 10 +++--- .../Trees/PartialViewTreeServiceTests.cs | 32 +++++++++++++++---- .../Services/Trees/ScriptTreeServiceTests.cs | 31 ++++++++++++++---- .../Trees/StyleSheetTreeServiceTests.cs | 32 +++++++++++++++---- 8 files changed, 90 insertions(+), 26 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/FileSystemTreeServiceBase.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/FileSystemTreeServiceBase.cs index 43816e77b23c..894c837dd8a5 100644 --- a/src/Umbraco.Cms.Api.Management/Services/FileSystem/FileSystemTreeServiceBase.cs +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/FileSystemTreeServiceBase.cs @@ -75,9 +75,12 @@ public string[] GetDirectories(string path) => FileSystem public string[] GetFiles(string path) => FileSystem .GetFiles(path) + .Where(FilterFile) .OrderBy(file => file) .ToArray(); + protected virtual bool FilterFile(string file) => true; + public bool DirectoryHasChildren(string path) => FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/PartialViewTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/PartialViewTreeService.cs index 3da299008cd9..7a614e69f397 100644 --- a/src/Umbraco.Cms.Api.Management/Services/FileSystem/PartialViewTreeService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/PartialViewTreeService.cs @@ -11,4 +11,6 @@ public class PartialViewTreeService : FileSystemTreeServiceBase, IPartialViewTre public PartialViewTreeService(FileSystems fileSystems) => _partialViewFileSystem = fileSystems.PartialViewsFileSystem ?? throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); + + protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".cshtml"); } diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/ScriptTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/ScriptTreeService.cs index ef870406f865..27b1eaf8f5e3 100644 --- a/src/Umbraco.Cms.Api.Management/Services/FileSystem/ScriptTreeService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/ScriptTreeService.cs @@ -11,4 +11,6 @@ public class ScriptTreeService : FileSystemTreeServiceBase, IScriptTreeService public ScriptTreeService(FileSystems fileSystems) => _scriptFileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); + + protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".js"); } diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/StyleSheetTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/StyleSheetTreeService.cs index ed14819231db..57cdf616aeae 100644 --- a/src/Umbraco.Cms.Api.Management/Services/FileSystem/StyleSheetTreeService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/StyleSheetTreeService.cs @@ -10,5 +10,7 @@ public class StyleSheetTreeService : FileSystemTreeServiceBase, IStyleSheetTreeS public StyleSheetTreeService(FileSystems fileSystems) => _scriptFileSystem = fileSystems.StylesheetsFileSystem ?? - throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); + throw new ArgumentException("Missing stylesheets file system", nameof(fileSystems)); + + protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".css"); } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/FileSystemTreeServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/FileSystemTreeServiceTestsBase.cs index e328b90d416b..0ae45feb110a 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/FileSystemTreeServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/FileSystemTreeServiceTestsBase.cs @@ -16,12 +16,14 @@ public abstract class FileSystemTreeServiceTestsBase : UmbracoIntegrationTest protected IFileSystem TestFileSystem { get; private set; } + protected abstract string FileExtension { get; set; } + protected abstract string FileSystemPath { get; } protected IHostingEnvironment HostingEnvironment => GetRequiredService(); [SetUp] - public void SetUpFileSystem() + public virtual void SetUpFileSystem() { TestFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, LoggerFactory.CreateLogger(), HostingEnvironment.MapPathWebRoot(FileSystemPath), HostingEnvironment.ToAbsolute(FileSystemPath)); @@ -37,11 +39,11 @@ public void SetUpFileSystem() for (int i = 0; i < 10; i++) { using var stream = CreateStream(Path.Join("tests")); - TestFileSystem.AddFile($"file{i}", stream); + TestFileSystem.AddFile($"file{i}{FileExtension}", stream); } } - private static Stream CreateStream(string contents = null) + protected static Stream CreateStream(string contents = null) { if (string.IsNullOrEmpty(contents)) { @@ -59,7 +61,7 @@ private static Stream CreateStream(string contents = null) protected virtual IFileSystem? GetScriptsFileSystem() => null; [TearDown] - public void TearDownFileSystem() + public virtual void TearDownFileSystem() { Purge(TestFileSystem, string.Empty); FileSystems = null; diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/PartialViewTreeServiceTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/PartialViewTreeServiceTests.cs index 0dbe9f39a347..f2068a4b7053 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/PartialViewTreeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/PartialViewTreeServiceTests.cs @@ -8,6 +8,8 @@ namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees; public class PartialViewTreeServiceTests : FileSystemTreeServiceTestsBase { + protected override string FileExtension { get; set; } = ".cshtml"; + protected override string FileSystemPath => Constants.SystemDirectories.PartialViews; protected override IFileSystem? GetPartialViewsFileSystem() => TestFileSystem; @@ -17,12 +19,12 @@ public void Can_Get_Siblings_From_PartialView_Tree_Service() { var service = new PartialViewTreeService(FileSystems); - FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after); - int index = Array.FindIndex(treeModel, item => item.Name == "file5"); + FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels($"file5{FileExtension}", 1, 1, out long before, out var after); + int index = Array.FindIndex(treeModel, item => item.Name == $"file5{FileExtension}"); - Assert.AreEqual(treeModel[index].Name, "file5"); - Assert.AreEqual(treeModel[index - 1].Name, "file4"); - Assert.AreEqual(treeModel[index + 1].Name, "file6"); + Assert.AreEqual(treeModel[index].Name, $"file5{FileExtension}"); + Assert.AreEqual(treeModel[index - 1].Name, $"file4{FileExtension}"); + Assert.AreEqual(treeModel[index + 1].Name, $"file6{FileExtension}"); Assert.That(treeModel.Length == 3); Assert.AreEqual(after, 3); Assert.AreEqual(before, 4); @@ -33,7 +35,7 @@ public void Can_Get_Ancestors_From_StyleSheet_Tree_Service() { var service = new PartialViewTreeService(FileSystems); - var path = Path.Join("tests", "file5"); + var path = Path.Join("tests", $"file5{FileExtension}"); FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true); Assert.IsNotEmpty(treeModel); @@ -46,9 +48,25 @@ public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service() { var service = new PartialViewTreeService(FileSystems); - FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems); + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, int.MaxValue, out var totalItems); Assert.IsNotEmpty(treeModels); Assert.AreEqual(treeModels.Length, totalItems); } + + [Test] + public void Will_Hide_Unsupported_File_Extensions() + { + var service = new PartialViewTreeService(FileSystems); + for (int i = 0; i < 2; i++) + { + using var stream = CreateStream(Path.Join("tests")); + TestFileSystem.AddFile($"file{i}.invalid", stream); + } + + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, int.MaxValue, out var totalItems); + + Assert.IsEmpty(treeModels.Where(file => file.Name.Contains(".invalid"))); + Assert.AreEqual(treeModels.Length, totalItems); + } } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/ScriptTreeServiceTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/ScriptTreeServiceTests.cs index 481fbd609bdf..874d59fd153a 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/ScriptTreeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/ScriptTreeServiceTests.cs @@ -7,6 +7,7 @@ namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees; public class ScriptTreeServiceTests : FileSystemTreeServiceTestsBase { + protected override string FileExtension { get; set; } = ".js"; protected override string FileSystemPath => GlobalSettings.UmbracoScriptsPath; protected override IFileSystem? GetScriptsFileSystem() => TestFileSystem; @@ -16,12 +17,12 @@ public void Can_Get_Siblings_From_Script_Tree_Service() { var service = new ScriptTreeService(FileSystems); - FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after); - int index = Array.FindIndex(treeModel, item => item.Name == "file5"); + FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels($"file5{FileExtension}", 1, 1, out long before, out var after); + int index = Array.FindIndex(treeModel, item => item.Name == $"file5{FileExtension}"); - Assert.AreEqual(treeModel[index].Name, "file5"); - Assert.AreEqual(treeModel[index - 1].Name, "file4"); - Assert.AreEqual(treeModel[index + 1].Name, "file6"); + Assert.AreEqual(treeModel[index].Name, $"file5{FileExtension}"); + Assert.AreEqual(treeModel[index - 1].Name, $"file4{FileExtension}"); + Assert.AreEqual(treeModel[index + 1].Name, $"file6{FileExtension}"); Assert.That(treeModel.Length == 3); Assert.AreEqual(after, 3); Assert.AreEqual(before, 4); @@ -32,7 +33,7 @@ public void Can_Get_Ancestors_From_StyleSheet_Tree_Service() { var service = new ScriptTreeService(FileSystems); - var path = Path.Join("tests", "file5"); + var path = Path.Join("tests", $"file5{FileExtension}"); FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true); Assert.IsNotEmpty(treeModel); @@ -45,9 +46,25 @@ public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service() { var service = new ScriptTreeService(FileSystems); - FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems); + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, int.MaxValue, out var totalItems); Assert.IsNotEmpty(treeModels); Assert.AreEqual(treeModels.Length, totalItems); } + + [Test] + public void Will_Hide_Unsupported_File_Extensions() + { + var service = new ScriptTreeService(FileSystems); + for (int i = 0; i < 2; i++) + { + using var stream = CreateStream(Path.Join("tests")); + TestFileSystem.AddFile($"file{i}.invalid", stream); + } + + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, int.MaxValue, out var totalItems); + + Assert.IsEmpty(treeModels.Where(file => file.Name.Contains(".invalid"))); + Assert.AreEqual(treeModels.Length, totalItems); + } } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/StyleSheetTreeServiceTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/StyleSheetTreeServiceTests.cs index 2fe15f27ea42..c3300bcbb8aa 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/StyleSheetTreeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/StyleSheetTreeServiceTests.cs @@ -7,6 +7,8 @@ namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees; public class StyleSheetTreeServiceTests : FileSystemTreeServiceTestsBase { + protected override string FileExtension { get; set; } = ".css"; + protected override string FileSystemPath => GlobalSettings.UmbracoCssPath; protected override IFileSystem? GetStylesheetsFileSystem() => TestFileSystem; @@ -16,12 +18,12 @@ public void Can_Get_Siblings_From_StyleSheet_Tree_Service() { var service = new StyleSheetTreeService(FileSystems); - FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after); - int index = Array.FindIndex(treeModel, item => item.Name == "file5"); + FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels($"file5{FileExtension}", 1, 1, out long before, out var after); + int index = Array.FindIndex(treeModel, item => item.Name == $"file5{FileExtension}"); - Assert.AreEqual(treeModel[index].Name, "file5"); - Assert.AreEqual(treeModel[index - 1].Name, "file4"); - Assert.AreEqual(treeModel[index + 1].Name, "file6"); + Assert.AreEqual(treeModel[index].Name, $"file5{FileExtension}"); + Assert.AreEqual(treeModel[index - 1].Name, $"file4{FileExtension}"); + Assert.AreEqual(treeModel[index + 1].Name, $"file6{FileExtension}"); Assert.That(treeModel.Length == 3); Assert.AreEqual(after, 3); Assert.AreEqual(before, 4); @@ -32,7 +34,7 @@ public void Can_Get_Ancestors_From_StyleSheet_Tree_Service() { var service = new StyleSheetTreeService(FileSystems); - var path = Path.Join("tests", "file5"); + var path = Path.Join("tests", $"file5{FileExtension}"); FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true); Assert.IsNotEmpty(treeModel); @@ -45,9 +47,25 @@ public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service() { var service = new StyleSheetTreeService(FileSystems); - FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems); + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, int.MaxValue, out var totalItems); Assert.IsNotEmpty(treeModels); Assert.AreEqual(treeModels.Length, totalItems); } + + [Test] + public void Will_Hide_Unsupported_File_Extensions() + { + var service = new StyleSheetTreeService(FileSystems); + for (int i = 0; i < 2; i++) + { + using var stream = CreateStream(Path.Join("tests")); + TestFileSystem.AddFile($"file{i}.invalid", stream); + } + + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, int.MaxValue, out var totalItems); + + Assert.IsEmpty(treeModels.Where(file => file.Name.Contains(".invalid"))); + Assert.AreEqual(treeModels.Length, totalItems); + } } From 6bc498ad41acb4cfcaecb52d05432e185cf1f894 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 22 Oct 2025 16:20:20 +0200 Subject: [PATCH 069/155] Trees: Restore backward compatibility for file system based tree controllers (closes #20602) (#20608) * Restore backward compatibility for file system based tree controllers. * Aligned obsoletion messages. --- .../AncestorsPartialViewTreeController.cs | 24 +++--- .../Tree/ChildrenPartialViewTreeController.cs | 25 +++--- .../Tree/PartialViewTreeControllerBase.cs | 22 +++-- .../Tree/RootPartialViewTreeController.cs | 25 +++--- .../Tree/SiblingsPartialViewTreeController.cs | 23 +++--- .../Tree/AncestorsScriptTreeController.cs | 23 +++--- .../Tree/ChildrenScriptTreeController.cs | 25 +++--- .../Script/Tree/RootScriptTreeController.cs | 25 +++--- .../Script/Tree/ScriptTreeControllerBase.cs | 22 +++-- .../Tree/SiblingsScriptTreeController.cs | 23 +++--- .../Tree/AncestorsStylesheetTreeController.cs | 23 +++--- .../Tree/ChildrenStylesheetTreeController.cs | 26 +++--- .../Tree/RootStylesheetTreeController.cs | 25 +++--- .../Tree/SiblingsStylesheetTreeController.cs | 23 +++--- .../Tree/StylesheetTreeControllerBase.cs | 20 ++--- .../Tree/FileSystemTreeControllerBase.cs | 81 ++++++++++++++----- 16 files changed, 225 insertions(+), 210 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs index 3f79545e3950..6f30a7268482 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs @@ -1,34 +1,32 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class AncestorsPartialViewTreeController : PartialViewTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) - : base(partialViewTreeService, fileSystems) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsPartialViewTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs index 099f01f3422d..9c70e7acbed4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) - : base(partialViewTreeService, fileSystems) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenPartialViewTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs index 6875e395f640..5f50a098983b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViews)] public class PartialViewTreeControllerBase : FileSystemTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + FileSystem = null!; + } - // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. - [ActivatorUtilitiesConstructor] - [Obsolete("Scheduled for removal in Umbraco 18.")] + // FileSystem is required therefore, we can't remove it without some wizardry. When obsoletion is due, remove this. + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) : base(partialViewTreeService) { - _partialViewTreeService = partialViewTreeService; FileSystem = fileSystems.PartialViewsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public PartialViewTreeControllerBase(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService()) + : base() => FileSystem = fileSystems.PartialViewsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); - [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs index 4e42266389f7..0a7d95627f87 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class RootPartialViewTreeController : PartialViewTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) - : base(partialViewTreeService, fileSystems) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootPartialViewTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs index af1e3171a643..8b3354d0f663 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs @@ -1,32 +1,31 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; public class SiblingsPartialViewTreeController : PartialViewTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) - : base(partialViewTreeService, fileSystems) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsPartialViewTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs index ed5acbc0c356..0aef76c5b782 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs @@ -1,10 +1,9 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; @@ -12,22 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class AncestorsScriptTreeController : ScriptTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public AncestorsScriptTreeController(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) - : base(scriptTreeService, fileSystems) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsScriptTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs index ba4003784142..8313b6294139 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class ChildrenScriptTreeController : ScriptTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public ChildrenScriptTreeController(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) - : base(scriptTreeService, fileSystems) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenScriptTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs index f29d3bdb4401..dcb5e2479ced 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class RootScriptTreeController : ScriptTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public RootScriptTreeController(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) - : base(scriptTreeService, fileSystems) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootScriptTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs index d79740d845e3..f803db096fd4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessScripts)] public class ScriptTreeControllerBase : FileSystemTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. public ScriptTreeControllerBase(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + FileSystem = null!; + } - // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. - [ActivatorUtilitiesConstructor] - [Obsolete("Scheduled for removal in Umbraco 18.")] + // FileSystem is required therefore, we can't remove it without some wizardry. When obsoletion is due, remove this. + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ScriptTreeControllerBase(IScriptTreeService scriptTreeService, FileSystems fileSystems) : base(scriptTreeService) { - _scriptTreeService = scriptTreeService; FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ScriptTreeControllerBase(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService()) + : base() => FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); - [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs index deec60cacb8e..fa53cfa77473 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs @@ -1,32 +1,31 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; public class SiblingsScriptTreeController : ScriptTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public SiblingsScriptTreeController(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) - : base(scriptTreeService, fileSystems) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsScriptTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs index 3760808263bf..2663152f4c6a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs @@ -1,10 +1,9 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; @@ -12,22 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class AncestorsStylesheetTreeController : StylesheetTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) - : base(styleSheetTreeService, fileSystems) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsStylesheetTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs index 41484bce50ec..bf016b54e39f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs @@ -1,36 +1,36 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class ChildrenStylesheetTreeController : StylesheetTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) - : base(styleSheetTreeService, fileSystems) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenStylesheetTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs index 417c636a375d..c47828d09228 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class RootStylesheetTreeController : StylesheetTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) - : base(styleSheetTreeService, fileSystems) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootStylesheetTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs index 0f2b03b704d9..b56afe70b775 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs @@ -1,32 +1,31 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; public class SiblingsStylesheetTreeController : StylesheetTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) - : base(styleSheetTreeService, fileSystems) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsStylesheetTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs index 501293f11fa4..07382a145399 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)] public class StylesheetTreeControllerBase : FileSystemTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + FileSystem = null!; + } // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. - [ActivatorUtilitiesConstructor] - [Obsolete("Scheduled for removal in Umbraco 18.")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) : base(styleSheetTreeService) { - _styleSheetTreeService = styleSheetTreeService; FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public StylesheetTreeControllerBase(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService()) + : base() => FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); - [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs index 68d1414788fb..fa3b2027b35d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs @@ -5,7 +5,6 @@ using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Extensions; @@ -13,23 +12,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree; public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase { - private readonly IFileSystemTreeService _fileSystemTreeService; + private readonly IFileSystemTreeService _fileSystemTreeService = null!; - [Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 18.")] + /// + /// Indicates whether to use the IFileSystemTreeService or the legacy implementation. + /// + /// + /// This is retained to ensure that any controllers outside of the CMS that use this base class with the obsolete constructor + /// continue to function until they can be updated to use the new service. + /// To be removed along with the constructor taking no parameters in Umbraco 19. + /// + private readonly bool _useFileSystemTreeService = true; + + [Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 19.")] protected abstract IFileSystem FileSystem { get; } [ActivatorUtilitiesConstructor] protected FileSystemTreeControllerBase(IFileSystemTreeService fileSystemTreeService) => _fileSystemTreeService = fileSystemTreeService; - [Obsolete("Use the other constructor. Scheduled for removal in Umbraco 18.")] - protected FileSystemTreeControllerBase() - : this(StaticServiceProvider.Instance.GetRequiredService()) - { - } + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")] + protected FileSystemTreeControllerBase() => _useFileSystemTreeService = false; protected Task>> GetRoot(int skip, int take) { - FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(string.Empty, skip, take, out var totalItems); + FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(string.Empty, skip, take, out var totalItems); PagedViewModel result = PagedViewModel(viewModels, totalItems); return Task.FromResult>>(Ok(result)); @@ -37,14 +43,14 @@ protected Task> protected Task>> GetChildren(string path, int skip, int take) { - FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(path, skip, take, out var totalItems); + FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(path, skip, take, out var totalItems); PagedViewModel result = PagedViewModel(viewModels, totalItems); return Task.FromResult>>(Ok(result)); } /// - /// Gets the sibling of the targeted item based on its path. + /// Gets the siblings of the targeted item based on its path. /// /// The path to the item. /// The amount of siblings you want to fetch from before the items position in the array. @@ -61,17 +67,19 @@ protected Task protected virtual Task>> GetAncestors(string path, bool includeSelf = true) { path = path.VirtualPathToSystemPath(); - FileSystemTreeItemPresentationModel[] models = _fileSystemTreeService.GetAncestorModels(path, includeSelf); + FileSystemTreeItemPresentationModel[] models = GetAncestorModels(path, includeSelf); return Task.FromResult>>(Ok(models)); } - private PagedViewModel PagedViewModel(IEnumerable viewModels, long totalItems) - => new() { Total = totalItems, Items = viewModels }; - - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) { + if (_useFileSystemTreeService) + { + return _fileSystemTreeService.GetAncestorModels(path, includeSelf); + } + var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray(); var result = directories .Select((directory, index) => MapViewModel(string.Join(Path.DirectorySeparatorChar, directories.Take(index + 1)), directory, true)) @@ -86,28 +94,59 @@ protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string return result.ToArray(); } - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] protected virtual string[] GetDirectories(string path) => FileSystem .GetDirectories(path) .OrderBy(directory => directory) .ToArray(); - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] protected virtual string[] GetFiles(string path) => FileSystem .GetFiles(path) .OrderBy(file => file) .ToArray(); - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] protected virtual bool DirectoryHasChildren(string path) => FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] private string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder ? Path.GetFileName(itemPath) : FileSystem.GetFileName(itemPath); - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + private FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems) + { + if (_useFileSystemTreeService) + { + return _fileSystemTreeService.GetPathViewModels(path, skip, take, out totalItems); + } + + path = path.VirtualPathToSystemPath(); + var allItems = GetDirectories(path) + .Select(directory => new { Path = directory, IsFolder = true }) + .Union(GetFiles(path).Select(file => new { Path = file, IsFolder = false })) + .ToArray(); + + totalItems = allItems.Length; + + FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder) + => MapViewModel( + itemPath, + GetFileSystemItemName(isFolder, itemPath), + isFolder); + + return allItems + .Skip(skip) + .Take(take) + .Select(item => ViewModel(item.Path, item.IsFolder)) + .ToArray(); + } + + private PagedViewModel PagedViewModel(IEnumerable viewModels, long totalItems) + => new() { Total = totalItems, Items = viewModels }; + + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] private FileSystemTreeItemPresentationModel MapViewModel(string path, string name, bool isFolder) { var parentPath = Path.GetDirectoryName(path); From 9cb59fe1b4c73c931148f24b35917ca711cf3b7b Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 22 Oct 2025 16:42:12 +0200 Subject: [PATCH 070/155] Bumped version to 16.3.3. --- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 74bf09f5a9cf..bbdb09451381 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.3.2", + "version": "16.3.3", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index 7167818c6d67..eb6387dfd627 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": "16.3.2", + "version": "16.3.3", "assemblyVersion": { "precision": "build" }, From a09e1777c4604d2b6b7a6c9616764c7a6522cedd Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 22 Oct 2025 12:21:42 +0200 Subject: [PATCH 071/155] Migrations: Use reliable GUID to check for existence of data type when creating (#20604) * Use reliable GUID to check for existence of data type in migration. * Retrieve just a single field in existence check. --- .../MigrateMediaTypeLabelProperties.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs index efa48f00f276..71c824f35758 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs @@ -6,7 +6,9 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_3_0; @@ -69,7 +71,7 @@ private void ToggleIndentityInsertForNodes(bool toggleOn) private void IfNotExistsCreateBytesLabel() { - if (Database.Exists(Constants.DataTypes.LabelBytes)) + if (NodeExists(_labelBytesDataTypeKey)) { return; } @@ -89,7 +91,7 @@ private void IfNotExistsCreateBytesLabel() CreateDate = DateTime.Now, }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); var dataTypeDto = new DataTypeDto { @@ -100,12 +102,12 @@ private void IfNotExistsCreateBytesLabel() Configuration = "{\"umbracoDataValueType\":\"BIGINT\", \"labelTemplate\":\"{=value | bytes}\"}", }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); } private void IfNotExistsCreatePixelsLabel() { - if (Database.Exists(Constants.DataTypes.LabelPixels)) + if (NodeExists(_labelPixelsDataTypeKey)) { return; } @@ -125,7 +127,7 @@ private void IfNotExistsCreatePixelsLabel() CreateDate = DateTime.Now, }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); var dataTypeDto = new DataTypeDto { @@ -136,7 +138,16 @@ private void IfNotExistsCreatePixelsLabel() Configuration = "{\"umbracoDataValueType\":\"INT\", \"labelTemplate\":\"{=value}px\"}", }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + + private bool NodeExists(Guid uniqueId) + { + Sql sql = Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.UniqueId == uniqueId); + return Database.FirstOrDefault(sql) is not null; } private async Task MigrateMediaTypeLabels() From 644334c63b79802b641fc2d63534e0ef337617f4 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 22 Oct 2025 16:20:20 +0200 Subject: [PATCH 072/155] Trees: Restore backward compatibility for file system based tree controllers (closes #20602) (#20608) * Restore backward compatibility for file system based tree controllers. * Aligned obsoletion messages. --- .../AncestorsPartialViewTreeController.cs | 24 +++--- .../Tree/ChildrenPartialViewTreeController.cs | 25 +++--- .../Tree/PartialViewTreeControllerBase.cs | 22 +++-- .../Tree/RootPartialViewTreeController.cs | 25 +++--- .../Tree/SiblingsPartialViewTreeController.cs | 23 +++--- .../Tree/AncestorsScriptTreeController.cs | 23 +++--- .../Tree/ChildrenScriptTreeController.cs | 25 +++--- .../Script/Tree/RootScriptTreeController.cs | 25 +++--- .../Script/Tree/ScriptTreeControllerBase.cs | 22 +++-- .../Tree/SiblingsScriptTreeController.cs | 23 +++--- .../Tree/AncestorsStylesheetTreeController.cs | 23 +++--- .../Tree/ChildrenStylesheetTreeController.cs | 26 +++--- .../Tree/RootStylesheetTreeController.cs | 25 +++--- .../Tree/SiblingsStylesheetTreeController.cs | 23 +++--- .../Tree/StylesheetTreeControllerBase.cs | 20 ++--- .../Tree/FileSystemTreeControllerBase.cs | 81 ++++++++++++++----- 16 files changed, 225 insertions(+), 210 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs index 3f79545e3950..6f30a7268482 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs @@ -1,34 +1,32 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class AncestorsPartialViewTreeController : PartialViewTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) - : base(partialViewTreeService, fileSystems) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsPartialViewTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs index 099f01f3422d..9c70e7acbed4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) - : base(partialViewTreeService, fileSystems) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenPartialViewTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs index 6875e395f640..5f50a098983b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViews)] public class PartialViewTreeControllerBase : FileSystemTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + FileSystem = null!; + } - // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. - [ActivatorUtilitiesConstructor] - [Obsolete("Scheduled for removal in Umbraco 18.")] + // FileSystem is required therefore, we can't remove it without some wizardry. When obsoletion is due, remove this. + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) : base(partialViewTreeService) { - _partialViewTreeService = partialViewTreeService; FileSystem = fileSystems.PartialViewsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public PartialViewTreeControllerBase(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService()) + : base() => FileSystem = fileSystems.PartialViewsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); - [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs index 4e42266389f7..0a7d95627f87 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class RootPartialViewTreeController : PartialViewTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) - : base(partialViewTreeService, fileSystems) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootPartialViewTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs index af1e3171a643..8b3354d0f663 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs @@ -1,32 +1,31 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; public class SiblingsPartialViewTreeController : PartialViewTreeControllerBase { - private readonly IPartialViewTreeService _partialViewTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService) - : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) - : base(partialViewTreeService, fileSystems) => - _partialViewTreeService = partialViewTreeService; + : base(partialViewTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsPartialViewTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs index ed5acbc0c356..0aef76c5b782 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs @@ -1,10 +1,9 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; @@ -12,22 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class AncestorsScriptTreeController : ScriptTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public AncestorsScriptTreeController(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) - : base(scriptTreeService, fileSystems) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsScriptTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs index ba4003784142..8313b6294139 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class ChildrenScriptTreeController : ScriptTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public ChildrenScriptTreeController(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) - : base(scriptTreeService, fileSystems) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenScriptTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs index f29d3bdb4401..dcb5e2479ced 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class RootScriptTreeController : ScriptTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public RootScriptTreeController(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) - : base(scriptTreeService, fileSystems) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootScriptTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs index d79740d845e3..f803db096fd4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessScripts)] public class ScriptTreeControllerBase : FileSystemTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. public ScriptTreeControllerBase(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + FileSystem = null!; + } - // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. - [ActivatorUtilitiesConstructor] - [Obsolete("Scheduled for removal in Umbraco 18.")] + // FileSystem is required therefore, we can't remove it without some wizardry. When obsoletion is due, remove this. + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ScriptTreeControllerBase(IScriptTreeService scriptTreeService, FileSystems fileSystems) : base(scriptTreeService) { - _scriptTreeService = scriptTreeService; FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ScriptTreeControllerBase(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService()) + : base() => FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); - [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs index deec60cacb8e..fa53cfa77473 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs @@ -1,32 +1,31 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; public class SiblingsScriptTreeController : ScriptTreeControllerBase { - private readonly IScriptTreeService _scriptTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public SiblingsScriptTreeController(IScriptTreeService scriptTreeService) - : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _scriptTreeService = scriptTreeService; + : base(scriptTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) - : base(scriptTreeService, fileSystems) => - _scriptTreeService = scriptTreeService; + : base(scriptTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsScriptTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs index 3760808263bf..2663152f4c6a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs @@ -1,10 +1,9 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; @@ -12,22 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class AncestorsStylesheetTreeController : StylesheetTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) - : base(styleSheetTreeService, fileSystems) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public AncestorsStylesheetTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs index 41484bce50ec..bf016b54e39f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs @@ -1,36 +1,36 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class ChildrenStylesheetTreeController : StylesheetTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) - : base(styleSheetTreeService, fileSystems) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public ChildrenStylesheetTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs index 417c636a375d..c47828d09228 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs @@ -1,34 +1,33 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class RootStylesheetTreeController : StylesheetTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) - : base(styleSheetTreeService, fileSystems) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public RootStylesheetTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs index 0f2b03b704d9..b56afe70b775 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs @@ -1,32 +1,31 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; public class SiblingsStylesheetTreeController : StylesheetTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + [ActivatorUtilitiesConstructor] public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) - => _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + } - [ActivatorUtilitiesConstructor] - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) - : base(styleSheetTreeService, fileSystems) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService, fileSystems) + { + } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public SiblingsStylesheetTreeController(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + : base(fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs index 501293f11fa4..07382a145399 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)] public class StylesheetTreeControllerBase : FileSystemTreeControllerBase { - private readonly IStyleSheetTreeService _styleSheetTreeService; - // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService) - : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) => - _styleSheetTreeService = styleSheetTreeService; + : base(styleSheetTreeService) + { + FileSystem = null!; + } // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. - [ActivatorUtilitiesConstructor] - [Obsolete("Scheduled for removal in Umbraco 18.")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) : base(styleSheetTreeService) { - _styleSheetTreeService = styleSheetTreeService; FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); } - [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")] public StylesheetTreeControllerBase(FileSystems fileSystems) - : this(StaticServiceProvider.Instance.GetRequiredService()) + : base() => FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); - [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")] + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs index 68d1414788fb..fa3b2027b35d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs @@ -5,7 +5,6 @@ using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Extensions; @@ -13,23 +12,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree; public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase { - private readonly IFileSystemTreeService _fileSystemTreeService; + private readonly IFileSystemTreeService _fileSystemTreeService = null!; - [Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 18.")] + /// + /// Indicates whether to use the IFileSystemTreeService or the legacy implementation. + /// + /// + /// This is retained to ensure that any controllers outside of the CMS that use this base class with the obsolete constructor + /// continue to function until they can be updated to use the new service. + /// To be removed along with the constructor taking no parameters in Umbraco 19. + /// + private readonly bool _useFileSystemTreeService = true; + + [Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 19.")] protected abstract IFileSystem FileSystem { get; } [ActivatorUtilitiesConstructor] protected FileSystemTreeControllerBase(IFileSystemTreeService fileSystemTreeService) => _fileSystemTreeService = fileSystemTreeService; - [Obsolete("Use the other constructor. Scheduled for removal in Umbraco 18.")] - protected FileSystemTreeControllerBase() - : this(StaticServiceProvider.Instance.GetRequiredService()) - { - } + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")] + protected FileSystemTreeControllerBase() => _useFileSystemTreeService = false; protected Task>> GetRoot(int skip, int take) { - FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(string.Empty, skip, take, out var totalItems); + FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(string.Empty, skip, take, out var totalItems); PagedViewModel result = PagedViewModel(viewModels, totalItems); return Task.FromResult>>(Ok(result)); @@ -37,14 +43,14 @@ protected Task> protected Task>> GetChildren(string path, int skip, int take) { - FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(path, skip, take, out var totalItems); + FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(path, skip, take, out var totalItems); PagedViewModel result = PagedViewModel(viewModels, totalItems); return Task.FromResult>>(Ok(result)); } /// - /// Gets the sibling of the targeted item based on its path. + /// Gets the siblings of the targeted item based on its path. /// /// The path to the item. /// The amount of siblings you want to fetch from before the items position in the array. @@ -61,17 +67,19 @@ protected Task protected virtual Task>> GetAncestors(string path, bool includeSelf = true) { path = path.VirtualPathToSystemPath(); - FileSystemTreeItemPresentationModel[] models = _fileSystemTreeService.GetAncestorModels(path, includeSelf); + FileSystemTreeItemPresentationModel[] models = GetAncestorModels(path, includeSelf); return Task.FromResult>>(Ok(models)); } - private PagedViewModel PagedViewModel(IEnumerable viewModels, long totalItems) - => new() { Total = totalItems, Items = viewModels }; - - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) { + if (_useFileSystemTreeService) + { + return _fileSystemTreeService.GetAncestorModels(path, includeSelf); + } + var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray(); var result = directories .Select((directory, index) => MapViewModel(string.Join(Path.DirectorySeparatorChar, directories.Take(index + 1)), directory, true)) @@ -86,28 +94,59 @@ protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string return result.ToArray(); } - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] protected virtual string[] GetDirectories(string path) => FileSystem .GetDirectories(path) .OrderBy(directory => directory) .ToArray(); - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] protected virtual string[] GetFiles(string path) => FileSystem .GetFiles(path) .OrderBy(file => file) .ToArray(); - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] protected virtual bool DirectoryHasChildren(string path) => FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] private string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder ? Path.GetFileName(itemPath) : FileSystem.GetFileName(itemPath); - [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")] + private FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems) + { + if (_useFileSystemTreeService) + { + return _fileSystemTreeService.GetPathViewModels(path, skip, take, out totalItems); + } + + path = path.VirtualPathToSystemPath(); + var allItems = GetDirectories(path) + .Select(directory => new { Path = directory, IsFolder = true }) + .Union(GetFiles(path).Select(file => new { Path = file, IsFolder = false })) + .ToArray(); + + totalItems = allItems.Length; + + FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder) + => MapViewModel( + itemPath, + GetFileSystemItemName(isFolder, itemPath), + isFolder); + + return allItems + .Skip(skip) + .Take(take) + .Select(item => ViewModel(item.Path, item.IsFolder)) + .ToArray(); + } + + private PagedViewModel PagedViewModel(IEnumerable viewModels, long totalItems) + => new() { Total = totalItems, Items = viewModels }; + + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")] private FileSystemTreeItemPresentationModel MapViewModel(string path, string name, bool isFolder) { var parentPath = Path.GetDirectoryName(path); From b7621355541a34aea9730cfb4dbc67846bbdf7fa Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 23 Oct 2025 11:21:25 +0200 Subject: [PATCH 073/155] Exclude 'release/no-notes' from release labels --- .github/workflows/label-to-release-announcement.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/label-to-release-announcement.yml b/.github/workflows/label-to-release-announcement.yml index 013b36d60c50..b8cd447b6e61 100644 --- a/.github/workflows/label-to-release-announcement.yml +++ b/.github/workflows/label-to-release-announcement.yml @@ -52,7 +52,7 @@ jobs: for (const item of items) { const releaseLabels = (item.labels || []) .map(l => (typeof l === "string" ? l : l.name)) // always get the name - .filter(n => typeof n === "string" && n.startsWith("release/")); + .filter(n => typeof n === "string" && n.startsWith("release/") && n !== "release/no-notes"); if (releaseLabels.length === 0) continue; core.info(`#${item.number}: ${releaseLabels.join(", ")}`); From 9cc2df7acd65e381cfa20ad6eefe886b1a42df21 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Thu, 23 Oct 2025 10:47:50 +0100 Subject: [PATCH 074/155] Preview: Removes sessions (#20561) Removes preview sessions concept Fixes #19443 and #19471. The implementation of exiting sessions was a design flawed. The v13 feature worked due to an implementation bug. Exiting preview mode should be a deliberate action by the user. --- .../apps/preview/apps/preview-exit.element.ts | 2 +- .../src/apps/preview/preview.context.ts | 62 ++----------------- .../src/apps/preview/preview.element.ts | 21 ------- 3 files changed, 7 insertions(+), 78 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts index 26af65490ba7..c1a45b753de2 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts @@ -6,7 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; export class UmbPreviewExitElement extends UmbLitElement { async #onClick() { const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT); - await previewContext?.exitPreview(0); + await previewContext?.exitPreview(); } override render() { diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts index 2acbd754dedf..f3f13c9fdeef 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts @@ -1,17 +1,15 @@ import { tryExecute } from '@umbraco-cms/backoffice/resources'; -import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api'; +import { HubConnectionBuilder } from '@umbraco-cms/backoffice/external/signalr'; import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'; +import type { HubConnection } from '@umbraco-cms/backoffice/external/signalr'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { HubConnectionBuilder, type HubConnection } from '@umbraco-cms/backoffice/external/signalr'; -import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; -import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; - -const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions'; interface UmbPreviewIframeArgs { className?: string; @@ -133,10 +131,6 @@ export class UmbPreviewContext extends UmbContextBase { return urlInfo.url.startsWith('/') ? `${this.#serverUrl}${urlInfo.url}` : urlInfo.url; } - #getSessionCount(): number { - return Math.max(Number(localStorage.getItem(UMB_LOCALSTORAGE_SESSION_KEY)), 0) || 0; - } - #setPreviewUrl(args?: UmbPreviewUrlArgs) { const host = args?.serverUrl || this.#serverUrl; const unique = args?.unique || this.#unique; @@ -179,35 +173,8 @@ export class UmbPreviewContext extends UmbContextBase { this.#previewUrl.setValue(previewUrlString); } - #setSessionCount(sessions: number) { - localStorage.setItem(UMB_LOCALSTORAGE_SESSION_KEY, sessions.toString()); - } - - checkSession() { - const sessions = this.#getSessionCount(); - if (sessions > 0) return; - - umbConfirmModal(this._host, { - headline: `Preview website?`, - content: `You have ended preview mode, do you want to enable it again to view the latest saved version of your website?`, - cancelLabel: 'View published version', - confirmLabel: 'Preview latest version', - }) - .then(() => { - this.restartSession(); - }) - .catch(() => { - this.exitSession(); - }); - } - - async exitPreview(sessions: number = 0) { - this.#setSessionCount(sessions); - - // We are good to end preview mode. - if (sessions <= 0) { - await this.#documentPreviewRepository.exit(); - } + async exitPreview() { + await this.#documentPreviewRepository.exit(); if (this.#connection) { await this.#connection.stop(); @@ -223,12 +190,6 @@ export class UmbPreviewContext extends UmbContextBase { window.location.replace(url); } - async exitSession() { - let sessions = this.#getSessionCount(); - sessions--; - this.exitPreview(sessions); - } - iframeLoaded(iframe: HTMLIFrameElement) { if (!iframe) return; this.#iframeReady.setValue(true); @@ -255,17 +216,6 @@ export class UmbPreviewContext extends UmbContextBase { document.location.reload(); } - async restartSession() { - await this.#documentPreviewRepository.enter(); - this.startSession(); - } - - startSession() { - let sessions = this.#getSessionCount(); - sessions++; - this.#setSessionCount(sessions); - } - #currentArgs: UmbPreviewIframeArgs = {}; async updateIFrame(args?: UmbPreviewIframeArgs) { const mergedArgs = { ...this.#currentArgs, ...args }; diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts index 4897f21190dc..b2d0db0da7aa 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts @@ -22,23 +22,6 @@ export class UmbPreviewElement extends UmbLitElement { this.observe(this.#context.previewUrl, (previewUrl) => (this._previewUrl = previewUrl)); } - override connectedCallback() { - super.connectedCallback(); - this.addEventListener('visibilitychange', this.#onVisibilityChange); - window.addEventListener('beforeunload', () => this.#context.exitSession()); - this.#context.startSession(); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - this.removeEventListener('visibilitychange', this.#onVisibilityChange); - // NOTE: Unsure how we remove an anonymous function from 'beforeunload' event listener. - // The reason for the anonymous function is that if we used a named function, - // `this` would be the `window` and would not have context to the class instance. [LK] - //window.removeEventListener('beforeunload', () => this.#context.exitSession()); - this.#context.exitSession(); - } - @state() private _iframeReady?: boolean; @@ -49,10 +32,6 @@ export class UmbPreviewElement extends UmbLitElement { this.#context.iframeLoaded(event.target); } - #onVisibilityChange() { - this.#context.checkSession(); - } - override render() { if (!this._previewUrl) return nothing; return html` From 08d217360e1bee8952d210f70734099b83a5fe49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:22:47 +0000 Subject: [PATCH 075/155] Bump vite from 7.1.9 to 7.1.11 in /src/Umbraco.Web.UI.Login Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.9 to 7.1.11. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.1.11 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- src/Umbraco.Web.UI.Login/package-lock.json | 8 ++++---- src/Umbraco.Web.UI.Login/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Login/package-lock.json b/src/Umbraco.Web.UI.Login/package-lock.json index 51ea289023aa..7a81a473af42 100644 --- a/src/Umbraco.Web.UI.Login/package-lock.json +++ b/src/Umbraco.Web.UI.Login/package-lock.json @@ -10,7 +10,7 @@ "@umbraco-cms/backoffice": "16.2.0", "msw": "^2.11.3", "typescript": "^5.9.3", - "vite": "^7.1.9" + "vite": "^7.1.11" }, "engines": { "node": ">=22", @@ -4328,9 +4328,9 @@ } }, "node_modules/vite": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", - "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Umbraco.Web.UI.Login/package.json b/src/Umbraco.Web.UI.Login/package.json index 8857826c05ec..e53f3f3c5fbe 100644 --- a/src/Umbraco.Web.UI.Login/package.json +++ b/src/Umbraco.Web.UI.Login/package.json @@ -18,7 +18,7 @@ "@umbraco-cms/backoffice": "16.2.0", "msw": "^2.11.3", "typescript": "^5.9.3", - "vite": "^7.1.9" + "vite": "^7.1.11" }, "msw": { "workerDirectory": [ From 3854b2bd53c76d639e3343691c04f812bcce7466 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 23 Oct 2025 14:32:37 +0200 Subject: [PATCH 076/155] Block List: Remove bold label from inline editing (#20437) Remove bold label from block list inline editing --- .../components/inline-list-block/inline-list-block.element.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts index fd379b6f28ba..b67c2e60eaea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts @@ -292,10 +292,6 @@ export class UmbInlineListBlockElement extends UmbLitElement { padding-left: var(--uui-size-2, 6px); } - #name { - font-weight: 700; - } - uui-tag { margin-left: 0.5em; margin-bottom: -0.3em; From e482976a9d218b49a7f7712ddb5498fc841ff680 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 23 Oct 2025 14:43:49 +0200 Subject: [PATCH 077/155] User And User Group Workspace: Make views extendable (#20548) (#20617) * implement user details as a workspace view * register user group details as a workspace view --- .../workspace/user-group/manifests.ts | 2 + .../user-group-workspace-editor.element.ts | 269 ++--------------- .../workspace/user-group/views/manifests.ts | 23 ++ ...er-group-details-workspace-view.element.ts | 275 ++++++++++++++++++ .../user/user/workspace/user/manifests.ts | 2 + .../user/user-workspace-editor.element.ts | 73 +---- .../user/workspace/user/views/manifests.ts | 23 ++ .../user-details-workspace-view.element.ts | 98 +++++++ 8 files changed, 444 insertions(+), 321 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/views/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/views/user-group-details-workspace-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/views/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/views/user-details-workspace-view.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/manifests.ts index 13c3dfb21346..ea9c5b869f43 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/manifests.ts @@ -1,4 +1,5 @@ import { UMB_USER_GROUP_WORKSPACE_ALIAS } from './constants.js'; +import { manifests as viewManifests } from './views/manifests.js'; import { UmbSubmitWorkspaceAction, UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; export const manifests: Array = [ @@ -30,4 +31,5 @@ export const manifests: Array = [ }, ], }, + ...viewManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts index cbbad3824f57..08aeea40244f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts @@ -1,27 +1,18 @@ -import type { UmbUserGroupDetailModel } from '../../types.js'; import { UMB_USER_GROUP_ROOT_WORKSPACE_PATH } from '../../paths.js'; +import type { UmbUserGroupDetailModel } from '../../types.js'; import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from './user-group-workspace.context-token.js'; -import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; -import { css, html, nothing, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UmbInputSectionElement } from '@umbraco-cms/backoffice/section'; -import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; -import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; -import type { UmbInputLanguageElement } from '@umbraco-cms/backoffice/language'; -import { UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/icon'; import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components'; - -import './components/user-group-entity-type-permission-groups.element.js'; +import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/icon'; +import { umbFocus, UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @customElement('umb-user-group-workspace-editor') export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { @state() private _isNew?: boolean = false; - @state() - private _unique?: UmbUserGroupDetailModel['unique']; - @state() private _name?: UmbUserGroupDetailModel['name']; @@ -34,41 +25,19 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { @state() private _icon?: UmbUserGroupDetailModel['icon']; - @state() - private _sections: UmbUserGroupDetailModel['sections'] = []; - - @state() - private _languages: UmbUserGroupDetailModel['languages'] = []; - - @state() - private _hasAccessToAllLanguages: UmbUserGroupDetailModel['hasAccessToAllLanguages'] = false; - - @state() - private _documentStartNode?: UmbUserGroupDetailModel['documentStartNode']; - - @state() - private _documentRootAccess: UmbUserGroupDetailModel['documentRootAccess'] = false; - - @state() - private _mediaStartNode?: UmbUserGroupDetailModel['mediaStartNode']; - - @state() - private _mediaRootAccess: UmbUserGroupDetailModel['mediaRootAccess'] = false; - #workspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; constructor() { super(); - this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => { - this.#workspaceContext = instance; + this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; this.#observeUserGroup(); }); } #observeUserGroup() { this.observe(this.#workspaceContext?.isNew, (value) => (this._isNew = value), '_observeIsNew'); - this.observe(this.#workspaceContext?.unique, (value) => (this._unique = value ?? undefined), '_observeUnique'); this.observe(this.#workspaceContext?.name, (value) => (this._name = value), '_observeName'); this.observe(this.#workspaceContext?.alias, (value) => (this._alias = value), '_observeAlias'); this.observe( @@ -77,102 +46,11 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { '_observeAliasCanBeChanged', ); this.observe(this.#workspaceContext?.icon, (value) => (this._icon = value), '_observeIcon'); - this.observe(this.#workspaceContext?.sections, (value) => (this._sections = value ?? []), '_observeSections'); - this.observe(this.#workspaceContext?.languages, (value) => (this._languages = value ?? []), '_observeLanguages'); - this.observe( - this.#workspaceContext?.hasAccessToAllLanguages, - (value) => (this._hasAccessToAllLanguages = value ?? false), - '_observeHasAccessToAllLanguages', - ); - - this.observe( - this.#workspaceContext?.documentRootAccess, - (value) => (this._documentRootAccess = value ?? false), - '_observeDocumentRootAccess', - ); - - this.observe( - this.#workspaceContext?.documentStartNode, - (value) => (this._documentStartNode = value), - '_observeDocumentStartNode', - ); - - this.observe( - this.#workspaceContext?.mediaRootAccess, - (value) => (this._mediaRootAccess = value ?? false), - '_observeMediaRootAccess', - ); - - this.observe( - this.#workspaceContext?.mediaStartNode, - (value) => (this._mediaStartNode = value), - '_observeMediaStartNode', - ); - } - - #onSectionsChange(event: UmbChangeEvent) { - event.stopPropagation(); - const target = event.target as UmbInputSectionElement; - // TODO make contexts method - this.#workspaceContext?.updateProperty('sections', target.selection); - } - - #onAllowAllLanguagesChange(event: UUIBooleanInputEvent) { - event.stopPropagation(); - const target = event.target; - // TODO make contexts method - this.#workspaceContext?.updateProperty('hasAccessToAllLanguages', target.checked); - } - - #onLanguagePermissionChange(event: UmbChangeEvent) { - event.stopPropagation(); - const target = event.target as UmbInputLanguageElement; - // TODO make contexts method - this.#workspaceContext?.updateProperty('languages', target.selection); - } - - #onAllowAllDocumentsChange(event: UUIBooleanInputEvent) { - event.stopPropagation(); - const target = event.target; - // TODO make contexts method - this.#workspaceContext?.updateProperty('documentRootAccess', target.checked); - this.#workspaceContext?.updateProperty('documentStartNode', null); } - #onDocumentStartNodeChange(event: CustomEvent) { - event.stopPropagation(); - // TODO: get back to this when documents have been decoupled from users. - // The event target is deliberately set to any to avoid an import cycle with documents. - const target = event.target as any; - const selected = target.selection?.[0]; - // TODO make contexts method - this.#workspaceContext?.updateProperty('documentStartNode', selected ? { unique: selected } : null); - } - - #onAllowAllMediaChange(event: UUIBooleanInputEvent) { - event.stopPropagation(); - const target = event.target; - // TODO make contexts method - this.#workspaceContext?.updateProperty('mediaRootAccess', target.checked); - this.#workspaceContext?.updateProperty('mediaStartNode', null); - } - - #onMediaStartNodeChange(event: CustomEvent) { - event.stopPropagation(); - // TODO: get back to this when media have been decoupled from users. - // The event target is deliberately set to any to avoid an import cycle with media. - const target = event.target as any; - const selected = target.selection?.[0]; - // TODO make contexts method - this.#workspaceContext?.updateProperty('mediaStartNode', selected ? { unique: selected } : null); - } - - override render() { - return html` - - ${this.#renderHeader()} ${this.#renderMain()} - - `; + #onNameAndAliasChange(event: InputEvent & { target: UmbInputWithAliasElement }) { + this.#workspaceContext?.updateProperty('name', event.target.value ?? ''); + this.#workspaceContext?.updateProperty('alias', event.target.alias ?? ''); } async #onIconClick() { @@ -193,9 +71,12 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { } } - #onNameAndAliasChange(event: InputEvent & { target: UmbInputWithAliasElement }) { - this.#workspaceContext?.updateProperty('name', event.target.value ?? ''); - this.#workspaceContext?.updateProperty('alias', event.target.alias ?? ''); + override render() { + return html` + + ${this.#renderHeader()} + + `; } #renderHeader() { @@ -219,118 +100,12 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { `; } - #renderMain() { - if (!this._unique) return nothing; - - return html` -
    - - -
    - - - - - - ${this.#renderLanguageAccess()} ${this.#renderDocumentAccess()} ${this.#renderMediaAccess()} -
    - - ${this.#renderPermissionGroups()} -
    -
    - `; - } - - #renderLanguageAccess() { - return html` - -
    - - - ${this._hasAccessToAllLanguages === false - ? html` - - ` - : nothing} -
    -
    - `; - } - - #renderDocumentAccess() { - return html` - -
    - -
    - - ${this._documentRootAccess === false - ? html` - - ` - : nothing} -
    - `; - } - - #renderMediaAccess() { - return html` - -
    - -
    - - ${this._mediaRootAccess === false - ? html` - - ` - : nothing} -
    - `; - } - - #renderPermissionGroups() { - return html` `; - } - static override styles = [ UmbTextStyles, css` :host { display: block; + width: 100%; height: 100%; } @@ -352,14 +127,6 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { flex: 1 1 auto; align-items: center; } - - #main { - padding: var(--uui-size-layout-1); - } - - uui-input { - width: 100%; - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/views/manifests.ts new file mode 100644 index 000000000000..91bacd1501cf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/views/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_USER_GROUP_WORKSPACE_ALIAS } from '../constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.UserGroup.Details', + name: 'User Group Details Workspace View', + element: () => import('./user-group-details-workspace-view.element.js'), + weight: 90, + meta: { + label: '#general_details', + pathname: 'details', + icon: 'edit', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_USER_GROUP_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/views/user-group-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/views/user-group-details-workspace-view.element.ts new file mode 100644 index 000000000000..1aaa43beb671 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/views/user-group-details-workspace-view.element.ts @@ -0,0 +1,275 @@ +import type { UmbUserGroupDetailModel } from '../../../types.js'; +import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context-token.js'; +import type { UmbInputSectionElement } from '@umbraco-cms/backoffice/section'; +import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import type { UmbInputLanguageElement } from '@umbraco-cms/backoffice/language'; +import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; + +import '../components/user-group-entity-type-permission-groups.element.js'; + +@customElement('umb-user-group-details-workspace-view') +export class UmbUserGroupDetailsWorkspaceViewElement extends UmbLitElement implements UmbWorkspaceViewElement { + @state() + private _unique?: UmbUserGroupDetailModel['unique']; + + @state() + private _sections: UmbUserGroupDetailModel['sections'] = []; + + @state() + private _languages: UmbUserGroupDetailModel['languages'] = []; + + @state() + private _hasAccessToAllLanguages: UmbUserGroupDetailModel['hasAccessToAllLanguages'] = false; + + @state() + private _documentStartNode?: UmbUserGroupDetailModel['documentStartNode']; + + @state() + private _documentRootAccess: UmbUserGroupDetailModel['documentRootAccess'] = false; + + @state() + private _mediaStartNode?: UmbUserGroupDetailModel['mediaStartNode']; + + @state() + private _mediaRootAccess: UmbUserGroupDetailModel['mediaRootAccess'] = false; + + #workspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + this.#observeUserGroup(); + }); + } + + #observeUserGroup() { + this.observe(this.#workspaceContext?.unique, (value) => (this._unique = value ?? undefined), '_observeUnique'); + this.observe(this.#workspaceContext?.sections, (value) => (this._sections = value ?? []), '_observeSections'); + this.observe(this.#workspaceContext?.languages, (value) => (this._languages = value ?? []), '_observeLanguages'); + this.observe( + this.#workspaceContext?.hasAccessToAllLanguages, + (value) => (this._hasAccessToAllLanguages = value ?? false), + '_observeHasAccessToAllLanguages', + ); + + this.observe( + this.#workspaceContext?.documentRootAccess, + (value) => (this._documentRootAccess = value ?? false), + '_observeDocumentRootAccess', + ); + + this.observe( + this.#workspaceContext?.documentStartNode, + (value) => (this._documentStartNode = value), + '_observeDocumentStartNode', + ); + + this.observe( + this.#workspaceContext?.mediaRootAccess, + (value) => (this._mediaRootAccess = value ?? false), + '_observeMediaRootAccess', + ); + + this.observe( + this.#workspaceContext?.mediaStartNode, + (value) => (this._mediaStartNode = value), + '_observeMediaStartNode', + ); + } + + #onSectionsChange(event: UmbChangeEvent) { + event.stopPropagation(); + const target = event.target as UmbInputSectionElement; + // TODO make contexts method + this.#workspaceContext?.updateProperty('sections', target.selection); + } + + #onAllowAllLanguagesChange(event: UUIBooleanInputEvent) { + event.stopPropagation(); + const target = event.target; + // TODO make contexts method + this.#workspaceContext?.updateProperty('hasAccessToAllLanguages', target.checked); + } + + #onLanguagePermissionChange(event: UmbChangeEvent) { + event.stopPropagation(); + const target = event.target as UmbInputLanguageElement; + // TODO make contexts method + this.#workspaceContext?.updateProperty('languages', target.selection); + } + + #onAllowAllDocumentsChange(event: UUIBooleanInputEvent) { + event.stopPropagation(); + const target = event.target; + // TODO make contexts method + this.#workspaceContext?.updateProperty('documentRootAccess', target.checked); + this.#workspaceContext?.updateProperty('documentStartNode', null); + } + + #onDocumentStartNodeChange(event: CustomEvent) { + event.stopPropagation(); + // TODO: get back to this when documents have been decoupled from users. + // The event target is deliberately set to any to avoid an import cycle with documents. + const target = event.target as any; + const selected = target.selection?.[0]; + // TODO make contexts method + this.#workspaceContext?.updateProperty('documentStartNode', selected ? { unique: selected } : null); + } + + #onAllowAllMediaChange(event: UUIBooleanInputEvent) { + event.stopPropagation(); + const target = event.target; + // TODO make contexts method + this.#workspaceContext?.updateProperty('mediaRootAccess', target.checked); + this.#workspaceContext?.updateProperty('mediaStartNode', null); + } + + #onMediaStartNodeChange(event: CustomEvent) { + event.stopPropagation(); + // TODO: get back to this when media have been decoupled from users. + // The event target is deliberately set to any to avoid an import cycle with media. + const target = event.target as any; + const selected = target.selection?.[0]; + // TODO make contexts method + this.#workspaceContext?.updateProperty('mediaStartNode', selected ? { unique: selected } : null); + } + + override render() { + if (!this._unique) return nothing; + + return html` +
    + + +
    + + + + + + ${this.#renderLanguageAccess()} ${this.#renderDocumentAccess()} ${this.#renderMediaAccess()} +
    + + ${this.#renderPermissionGroups()} +
    +
    + `; + } + + #renderLanguageAccess() { + return html` + +
    + + + ${this._hasAccessToAllLanguages === false + ? html` + + ` + : nothing} +
    +
    + `; + } + + #renderDocumentAccess() { + return html` + +
    + +
    + + ${this._documentRootAccess === false + ? html` + + ` + : nothing} +
    + `; + } + + #renderMediaAccess() { + return html` + +
    + +
    + + ${this._mediaRootAccess === false + ? html` + + ` + : nothing} +
    + `; + } + + #renderPermissionGroups() { + return html` `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + height: 100%; + } + + #main { + padding: var(--uui-size-layout-1); + } + + uui-input { + width: 100%; + } + `, + ]; +} + +export { UmbUserGroupDetailsWorkspaceViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-group-details-workspace-view': UmbUserGroupDetailsWorkspaceViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/manifests.ts index 850cee09f912..0dc237d29329 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/manifests.ts @@ -1,6 +1,7 @@ import { UMB_USER_ENTITY_TYPE } from '../../entity.js'; import { UMB_USER_WORKSPACE_ALIAS } from './constants.js'; import { UMB_WORKSPACE_CONDITION_ALIAS, UmbSubmitWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; +import { manifests as viewManifests } from './views/manifests.js'; export const manifests: Array = [ { @@ -31,4 +32,5 @@ export const manifests: Array = [ }, ], }, + ...viewManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace-editor.element.ts index d0bf6b4cef35..ffc11e434da2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace-editor.element.ts @@ -1,93 +1,26 @@ -import type { UmbUserDetailModel } from '../../index.js'; import { UMB_USER_ROOT_WORKSPACE_PATH } from '../../paths.js'; -import type { UmbUserWorkspaceContext } from './user-workspace.context.js'; -import { UMB_USER_WORKSPACE_CONTEXT } from './user-workspace.context-token.js'; -import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -// import local components. Theses are not meant to be used outside of this component. -import './components/user-workspace-profile-settings/user-workspace-profile-settings.element.js'; -import './components/user-workspace-access/user-workspace-access.element.js'; -import './components/user-workspace-info/user-workspace-info.element.js'; -import './components/user-workspace-avatar/user-workspace-avatar.element.js'; -import './components/user-workspace-client-credentials/user-workspace-client-credentials.element.js'; - @customElement('umb-user-workspace-editor') export class UmbUserWorkspaceEditorElement extends UmbLitElement { - @state() - private _user?: UmbUserDetailModel; - - #workspaceContext?: UmbUserWorkspaceContext; - - constructor() { - super(); - - this.consumeContext(UMB_USER_WORKSPACE_CONTEXT, (context) => { - this.#workspaceContext = context; - this.#observeUser(); - }); - } - - #observeUser() { - if (!this.#workspaceContext) return; - this.observe(this.#workspaceContext.data, (user) => (this._user = user), 'umbUserObserver'); - } - override render() { return html` - + - ${this._user - ? html`
    -
    ${this.#renderLeftColumn()}
    -
    ${this.#renderRightColumn()}
    -
    ` - : nothing}
    `; } - #renderLeftColumn() { - return html` - - - - - - `; - } - - #renderRightColumn() { - return html` - - - - - - `; - } - static override styles = [ UmbTextStyles, css` :host { display: block; + width: 100%; height: 100%; } - - #main { - display: grid; - grid-template-columns: 1fr 350px; - gap: var(--uui-size-layout-1); - padding: var(--uui-size-layout-1); - } - - #left-column { - display: flex; - flex-direction: column; - gap: var(--uui-size-space-4); - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/views/manifests.ts new file mode 100644 index 000000000000..3e414288d594 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/views/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_USER_WORKSPACE_ALIAS } from '../constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.User.Details', + name: 'User Details Workspace View', + element: () => import('./user-details-workspace-view.element.js'), + weight: 90, + meta: { + label: '#general_details', + pathname: 'details', + icon: 'edit', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_USER_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/views/user-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/views/user-details-workspace-view.element.ts new file mode 100644 index 000000000000..292639178b33 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/views/user-details-workspace-view.element.ts @@ -0,0 +1,98 @@ +import { UMB_USER_WORKSPACE_CONTEXT } from '../user-workspace.context-token.js'; +import type { UmbUserWorkspaceContext } from '../user-workspace.context.js'; +import type { UmbUserDetailModel } from '../../../types.js'; +import { customElement, html, nothing, state, css } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +// import local components. Theses are not meant to be used outside of this component. +import '../components/user-workspace-profile-settings/user-workspace-profile-settings.element.js'; +import '../components/user-workspace-access/user-workspace-access.element.js'; +import '../components/user-workspace-info/user-workspace-info.element.js'; +import '../components/user-workspace-avatar/user-workspace-avatar.element.js'; +import '../components/user-workspace-client-credentials/user-workspace-client-credentials.element.js'; + +@customElement('umb-user-details-workspace-view') +export class UmbUserDetailsWorkspaceViewElement extends UmbLitElement implements UmbWorkspaceViewElement { + @state() + private _user?: UmbUserDetailModel; + + #workspaceContext?: UmbUserWorkspaceContext; + + constructor() { + super(); + + this.consumeContext(UMB_USER_WORKSPACE_CONTEXT, (context) => { + this.#workspaceContext = context; + this.#observeUser(); + }); + } + + #observeUser() { + if (!this.#workspaceContext) return; + this.observe(this.#workspaceContext.data, (user) => (this._user = user), 'umbUserObserver'); + } + + override render() { + return html` + ${this._user + ? html`
    +
    ${this.#renderLeftColumn()}
    +
    ${this.#renderRightColumn()}
    +
    ` + : nothing} + `; + } + + #renderLeftColumn() { + return html` + + + + + + `; + } + + #renderRightColumn() { + return html` + + + + + + `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: block; + height: 100%; + } + + #main { + display: grid; + grid-template-columns: 1fr 350px; + gap: var(--uui-size-layout-1); + padding: var(--uui-size-layout-1); + } + + #left-column { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-4); + } + `, + ]; +} + +export { UmbUserDetailsWorkspaceViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-details-workspace-view': UmbUserDetailsWorkspaceViewElement; + } +} From 8434c7d0cbf919532be72e0c86bfed5a3af66877 Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:49:14 +0200 Subject: [PATCH 078/155] Icon Picker: Fix empty selection allowed on mandatory fields and add validation. (#20536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Not show the empty tile when filtering is active. * Added mandatory property to the icon picker. * Avoid deselecting the icon on second click when not showing the empty option. * Extends the form control mixin to the icon picker. * Used super.value. * Support mandatory from settings config. * Removed mandatoryConf. * remove requestUpdate --------- Co-authored-by: Niels Lyngsø --- .../icon-picker-modal.element.ts | 38 +++++++++++++------ .../icon-picker-modal.token.ts | 1 + .../property-editor-ui-icon-picker.element.ts | 37 +++++++++++++----- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts index 0311318845ae..7227be73a0d3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-picker-modal/icon-picker-modal.element.ts @@ -30,6 +30,9 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement !color.legacy); + @state() + private _isSearching = false; + constructor() { super(); this.consumeContext(UMB_ICON_REGISTRY_CONTEXT, (context) => { @@ -44,8 +47,10 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement 0; this._iconsFiltered = this.#icons.filter((icon) => icon.name.toLowerCase().includes(value.toLowerCase())); } else { + this._isSearching = false; this._iconsFiltered = this.#icons; } } @@ -54,8 +59,12 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement
    - { - if (e.key === 'Enter' || e.key === ' ') this.#clearIcon(); - }}> - ${this.renderIcons()} { + if (e.key === 'Enter' || e.key === ' ') this.#clearIcon(); + }}> + + + ` + : nothing} + ${this.renderIcons()} (UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ + @property({ type: Boolean }) + mandatory = false; + + protected override firstUpdated(): void { + this.addValidator( + 'valueMissing', + () => 'Icon is required', + () => this.mandatory && !this._icon, + ); + } + @property() - public set value(v: string) { - this._value = v ?? ''; - const parts = this._value.split(' '); + public override set value(v: string) { + const val = v ?? ''; + super.value = val; + + const parts = val.split(' '); if (parts.length === 2) { this._icon = parts[0]; this._color = parts[1].replace('color-', ''); } else { - this._icon = this._value; + this._icon = val; this._color = ''; } } - public get value() { - return this._value; + + public override get value() { + return (super.value as string) ?? ''; } - private _value = ''; @state() private _icon = ''; @@ -53,7 +70,7 @@ export class UmbPropertyEditorUIIconPickerElement extends UmbLitElement implemen icon: this._icon, color: this._color, }, - data: { placeholder: this._placeholderIcon }, + data: { placeholder: this._placeholderIcon, showEmptyOption: !this.mandatory }, }).catch(() => undefined); if (!data) return; From d9c201e3d17265591681814b7782d13630ce37b1 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 24 Oct 2025 08:36:34 +0200 Subject: [PATCH 079/155] docs: Add backoffice preview URL to README files (#20623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Add backoffice preview URL to README files Added links to https://backofficepreview.umbraco.com/ in both the main repository README and the Umbraco.Web.UI.Client package README to make the live backoffice preview easily discoverable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * docs: fix link * Update .github/README.md Co-authored-by: Andy Butland --------- Co-authored-by: Claude Co-authored-by: Andy Butland --- .github/README.md | 8 ++++++++ src/Umbraco.Web.UI.Client/README.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/.github/README.md b/.github/README.md index 195d4f9a36c7..33cb2d41d6e2 100644 --- a/.github/README.md +++ b/.github/README.md @@ -38,6 +38,14 @@ Some important documentation links to get you started: - [Getting to know Umbraco](https://docs.umbraco.com/umbraco-cms/fundamentals/get-to-know-umbraco) - [Tutorials for creating a basic website and customizing the editing experience](https://docs.umbraco.com/umbraco-cms/tutorials/overview) +## Backoffice Preview + +Want to see the latest backoffice UI in action? Check out our live preview: + +**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)** + +This preview is automatically deployed from the main branch and showcases the latest backoffice features and improvements. It runs from mock data and persistent edits are not supported. + ## Get help If you need a bit of feedback while building your Umbraco projects, we are [chatty on Discord](https://discord.umbraco.com). Our Discord server serves as a social space for all Umbracians. If you have any questions or need some help with a problem, head over to our [dedicated forum](https://forum.umbraco.com/) where the Umbraco Community will be happy to help. diff --git a/src/Umbraco.Web.UI.Client/README.md b/src/Umbraco.Web.UI.Client/README.md index 1404fba49ca8..0bc565c8edab 100644 --- a/src/Umbraco.Web.UI.Client/README.md +++ b/src/Umbraco.Web.UI.Client/README.md @@ -2,6 +2,14 @@ This package contains the types for the Umbraco Backoffice. +## Preview + +A live preview of the latest backoffice build from the main branch is available at: + +**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)** + +This preview is automatically deployed via GitHub Actions whenever changes are pushed to main or version branches. + ## Installation ```bash From d9cdf03442fb41b5f71683ae140ba88ce37d69bc Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Fri, 24 Oct 2025 09:29:22 +0100 Subject: [PATCH 080/155] Preview: Allows changing the preview environment inside the preview app, and other UX changes that enhance the experience (#20598) * Preview Device: refactored config Fixed "flip" icon style. Removed "shadow" as unnecessary. Renamed "className" to "wrapperClass" to be descriptive. * Preview element CSS refinement * Preview element: load in private extensions * Added "Preview Environments" preview-app Made `unique`, `culture` and `segment` observable in the context. * Aligned preview-app design with `hidden` attribute and design consistency. * Created "Preview" package * Relocated "Preview Apps" and Context to the new package * Deprecated `UmbDocumentPreviewRepository` (for v19) as the methods have moved to `UmbPreviewRepository`. * Removed Preview Sessions event listeners * Changed localization from "End" to "Exit" * chore: consumes context only when needed * feat: uses the UmbPreviewRepository instead * feat: adds localization to errors and ensures the function does not randomly throw * feat: prevents creating a new repository for every click * feat: prevents potential memory leak by adding a signal to the events added to each iframe update * feat: adds a custom interface to prevent typescript errors * feat: ensures new string states are checked properly * docs: adds comment to avoid confusion * feat: sets up scaling once per iframe load rather than on each update * fix: ensures that you can go back to the default segment again * feat: closes popovers when clicking on the iframe (losing blur) and if selecting an item (expect for devices) --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- src/Umbraco.Web.UI.Client/package-lock.json | 7 + src/Umbraco.Web.UI.Client/package.json | 1 + .../src/apps/preview/index.ts | 1 + .../src/apps/preview/preview.element.ts | 68 ++++--- .../src/assets/lang/en.ts | 4 +- .../preview/document-preview.repository.ts | 13 ++ .../workspace/document-workspace.context.ts | 4 +- .../src/packages/preview/context/index.ts | 2 + .../preview/context/preview.context-token.ts | 4 + .../preview/context}/preview.context.ts | 150 ++++++++++------ .../src/packages/preview/index.ts | 2 + .../src/packages/preview/manifests.ts | 4 + .../src/packages/preview/package.json | 8 + .../preview/preview-apps}/manifests.ts | 15 +- .../preview-apps}/preview-culture.element.ts | 55 +++++- .../preview-apps}/preview-device.element.ts | 87 ++++++--- .../preview-environments.element.ts | 170 ++++++++++++++++++ .../preview-apps}/preview-exit.element.ts | 4 +- .../preview-open-website.element.ts | 4 +- .../preview-apps}/preview-segment.element.ts | 66 +++++-- .../packages/preview/preview-apps/types.ts | 6 + .../src/packages/preview/repository/index.ts | 1 + .../preview/repository/preview.repository.ts | 74 ++++++++ .../src/packages/preview/umbraco-package.ts | 9 + .../src/packages/preview/vite.config.ts | 12 ++ src/Umbraco.Web.UI.Client/tsconfig.json | 1 + 26 files changed, 641 insertions(+), 131 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/apps/preview/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/context/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context-token.ts rename src/Umbraco.Web.UI.Client/src/{apps/preview => packages/preview/context}/preview.context.ts (65%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/package.json rename src/Umbraco.Web.UI.Client/src/{apps/preview/apps => packages/preview/preview-apps}/manifests.ts (76%) rename src/Umbraco.Web.UI.Client/src/{apps/preview/apps => packages/preview/preview-apps}/preview-culture.element.ts (66%) rename src/Umbraco.Web.UI.Client/src/{apps/preview/apps => packages/preview/preview-apps}/preview-device.element.ts (57%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts rename src/Umbraco.Web.UI.Client/src/{apps/preview/apps => packages/preview/preview-apps}/preview-exit.element.ts (91%) rename src/Umbraco.Web.UI.Client/src/{apps/preview/apps => packages/preview/preview-apps}/preview-open-website.element.ts (91%) rename src/Umbraco.Web.UI.Client/src/{apps/preview/apps => packages/preview/preview-apps}/preview-segment.element.ts (62%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/repository/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/repository/preview.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/umbraco-package.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/preview/vite.config.ts diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 9c08dfb59fe6..74589de99c53 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -3638,6 +3638,10 @@ "resolved": "src/packages/performance-profiling", "link": true }, + "node_modules/@umbraco-backoffice/preview": { + "resolved": "src/packages/preview", + "link": true + }, "node_modules/@umbraco-backoffice/property-editors": { "resolved": "src/packages/property-editors", "link": true @@ -17084,6 +17088,9 @@ "src/packages/performance-profiling": { "name": "@umbraco-backoffice/performance-profiling" }, + "src/packages/preview": { + "name": "@umbraco-backoffice/preview" + }, "src/packages/property-editors": { "name": "@umbraco-backoffice/property-editors" }, diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index b22f6f4e7456..f91e42d9ba7d 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -85,6 +85,7 @@ "./picker-input": "./dist-cms/packages/core/picker-input/index.js", "./picker-data-source": "./dist-cms/packages/core/picker-data-source/index.js", "./picker": "./dist-cms/packages/core/picker/index.js", + "./preview": "./dist-cms/packages/preview/index.js", "./property-action": "./dist-cms/packages/core/property-action/index.js", "./property-editor-data-source": "./dist-cms/packages/core/property-editor-data-source/index.js", "./property-editor": "./dist-cms/packages/core/property-editor/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/index.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/index.ts new file mode 100644 index 000000000000..d90c7995f9bb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/index.ts @@ -0,0 +1 @@ +export * from './preview.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts index b2d0db0da7aa..e9bce3c441a0 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts @@ -1,8 +1,14 @@ -import { manifests as previewApps } from './apps/manifests.js'; -import { UmbPreviewContext } from './preview.context.js'; import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { + umbExtensionsRegistry, + UmbBackofficeEntryPointExtensionInitializer, +} from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbPreviewContext } from '@umbraco-cms/backoffice/preview'; +import { UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api'; +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; + +const CORE_PACKAGES = [import('../../packages/preview/umbraco-package.js')]; /** * @element umb-preview @@ -11,22 +17,39 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; export class UmbPreviewElement extends UmbLitElement { #context = new UmbPreviewContext(this); + @state() + private _iframeReady?: boolean; + + @state() + private _previewUrl?: string; + constructor() { super(); - if (previewApps?.length) { - umbExtensionsRegistry.registerMany(previewApps); - } + new UmbBackofficeEntryPointExtensionInitializer(this, umbExtensionsRegistry); this.observe(this.#context.iframeReady, (iframeReady) => (this._iframeReady = iframeReady)); this.observe(this.#context.previewUrl, (previewUrl) => (this._previewUrl = previewUrl)); } - @state() - private _iframeReady?: boolean; + override async firstUpdated() { + await this.#extensionsAfterAuth(); - @state() - private _previewUrl?: string; + // Extensions are loaded in parallel and don't need to block the preview frame + CORE_PACKAGES.forEach(async (packageImport) => { + const { extensions } = await packageImport; + umbExtensionsRegistry.registerMany(extensions); + }); + } + + async #extensionsAfterAuth() { + const authContext = await this.getContext(UMB_AUTH_CONTEXT, { preventTimeout: true }); + if (!authContext) { + throw new Error('UmbPreviewElement requires the UMB_AUTH_CONTEXT to be set.'); + } + await this.observe(authContext.isAuthorized).asPromise(); + await new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions(); + } #onIFrameLoad(event: Event & { target: HTMLIFrameElement }) { this.#context.iframeLoaded(event.target); @@ -36,7 +59,7 @@ export class UmbPreviewElement extends UmbLitElement { if (!this._previewUrl) return nothing; return html` ${when(!this._iframeReady, () => html`
    `)} -
    +
    + sandbox="allow-scripts allow-same-origin allow-forms">
    From 04918ec3d27bba9a38b900b4d10df2060588241e Mon Sep 17 00:00:00 2001 From: Luuk Peters Date: Sun, 9 Nov 2025 22:38:16 +0100 Subject: [PATCH 124/155] Slider property editor: Fix for preset value handling of `enableRange` (#20772) Fix config value access in UmbSliderPropertyValuePreset Updated the `UmbSliderPropertyValuePreset` class to ensure the `.value` property is accessed for configuration items. This change improves the accuracy of retrieving `enableRange`, `min`, `max`, and `step` values, addressing potential bugs in value processing. Co-authored-by: Luuk Peters --- .../slider/slider-property-value-preset.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/slider-property-value-preset.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/slider-property-value-preset.ts index 8c27b41147d3..acbec3f810ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/slider-property-value-preset.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/slider-property-value-preset.ts @@ -6,16 +6,16 @@ export class UmbSliderPropertyValuePreset implements UmbPropertyValuePreset { async processValue(value: undefined | UmbSliderPropertyEditorUiValue, config: UmbPropertyEditorConfig) { - const enableRange = Boolean(config.find((x) => x.alias === 'enableRange') ?? false); + const enableRange = Boolean(config.find((x) => x.alias === 'enableRange')?.value ?? false); /* - const min = Number(config.find((x) => x.alias === 'minVal') ?? 0); - const max = Number(config.find((x) => x.alias === 'maxVal') ?? 100); + const min = Number(config.find((x) => x.alias === 'minVal')?.value ?? 0); + const max = Number(config.find((x) => x.alias === 'maxVal')?.value ?? 100); const minVerified = isNaN(min) ? undefined : min; const maxVerified = isNaN(max) ? undefined : max; */ - const step = (config.find((x) => x.alias === 'step') as number | undefined) ?? 0; + const step = Number(config.find((x) => x.alias === 'step')?.value ?? 0); const stepVerified = step > 0 ? step : 1; const initValueMin = Number(config.find((x) => x.alias === 'initVal1')?.value) || 0; From fcfaff9daab89f9a90075bc74c206811c3fecb05 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 10 Nov 2025 03:02:10 +0100 Subject: [PATCH 125/155] Querying: Restore ability to retrieve all children published in any culture (closes #20760) (#20766) * Restore ability to retrieve all children published in any culture. * Fixed typo in test name. --- .../PublishStatus/PublishStatusService.cs | 4 +- .../PublishedContentStatusFilteringService.cs | 9 +-- ...ishedContentStatusFilteringServiceTests.cs | 72 ++++++++++++++----- 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs b/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs index 63d6d657fc80..43b954a7f7b5 100644 --- a/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs +++ b/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs @@ -69,7 +69,9 @@ public bool IsDocumentPublished(Guid documentKey, string culture) if (_publishedCultures.TryGetValue(documentKey, out ISet? publishedCultures)) { - return publishedCultures.Contains(culture, StringComparer.InvariantCultureIgnoreCase); + // If "*" is provided as the culture, we consider this as "published in any culture". This aligns + // with behaviour in Umbraco 13. + return culture == Constants.System.InvariantCulture || publishedCultures.Contains(culture, StringComparer.InvariantCultureIgnoreCase); } _logger.LogDebug("Document {DocumentKey} not found in the publish status cache", documentKey); diff --git a/src/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringService.cs b/src/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringService.cs index 860b4cb2f3e2..fce1eb949c75 100644 --- a/src/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringService.cs +++ b/src/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringService.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; @@ -40,13 +40,14 @@ public IEnumerable FilterAvailable(IEnumerable candidat _publishStatusQueryService.IsDocumentPublished(key, culture) && _publishStatusQueryService.HasPublishedAncestorPath(key)); - return WhereIsInvariantOrHasCulture(candidateKeys, culture, preview).ToArray(); + return WhereIsInvariantOrHasCultureOrRequestedAllCultures(candidateKeys, culture, preview).ToArray(); } - private IEnumerable WhereIsInvariantOrHasCulture(IEnumerable keys, string culture, bool preview) + private IEnumerable WhereIsInvariantOrHasCultureOrRequestedAllCultures(IEnumerable keys, string culture, bool preview) => keys .Select(key => _publishedContentCache.GetById(preview, key)) .WhereNotNull() - .Where(content => content.ContentType.VariesByCulture() is false + .Where(content => culture == Constants.System.InvariantCulture + || content.ContentType.VariesByCulture() is false || content.Cultures.ContainsKey(culture)); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs index 6949d47c8883..d73fff433b74 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs @@ -1,5 +1,6 @@ -using Moq; +using Moq; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -44,9 +45,10 @@ public void FilterAvailable_Invariant_ForPreview_YieldsUnpublishedItems() [TestCase("da-DK", 3)] [TestCase("en-US", 4)] + [TestCase("*", 5)] public void FilterAvailable_Variant_ForNonPreview_YieldsPublishedItemsInCulture(string culture, int expectedNumberOfChildren) { - var (sut, items) = SetupVariant(false, culture); + var (sut, items) = SetupVariant(false, culture == Constants.System.InvariantCulture ? "en-US" : culture); var children = sut.FilterAvailable(items.Keys, culture).ToArray(); Assert.AreEqual(expectedNumberOfChildren, children.Length); @@ -70,16 +72,24 @@ public void FilterAvailable_Variant_ForNonPreview_YieldsPublishedItemsInCulture( { Assert.AreEqual(8, children[2].Id); } + + if (culture == Constants.System.InvariantCulture) + { + Assert.AreEqual(4, children[2].Id); + Assert.AreEqual(6, children[3].Id); + Assert.AreEqual(8, children[4].Id); + } } - [TestCase("da-DK")] - [TestCase("en-US")] - public void FilterAvailable_Variant_ForPreview_YieldsUnpublishedItemsInCulture(string culture) + [TestCase("da-DK", 7)] + [TestCase("en-US", 7)] + [TestCase("*", 10)] + public void FilterAvailable_Variant_ForPreview_YieldsUnpublishedItemsInCulture(string culture, int expectedNumberOfChildren) { - var (sut, items) = SetupVariant(true, culture); + var (sut, items) = SetupVariant(true, culture == Constants.System.InvariantCulture ? "en-US" : culture); var children = sut.FilterAvailable(items.Keys, culture).ToArray(); - Assert.AreEqual(7, children.Length); + Assert.AreEqual(expectedNumberOfChildren, children.Length); // IDs 0 through 3 exist in both en-US and da-DK Assert.Multiple(() => @@ -105,16 +115,27 @@ public void FilterAvailable_Variant_ForPreview_YieldsUnpublishedItemsInCulture(s Assert.AreEqual(8, children[5].Id); Assert.AreEqual(9, children[6].Id); } + + if (culture == Constants.System.InvariantCulture) + { + Assert.AreEqual(4, children[4].Id); + Assert.AreEqual(5, children[5].Id); + Assert.AreEqual(6, children[6].Id); + Assert.AreEqual(7, children[7].Id); + Assert.AreEqual(8, children[8].Id); + Assert.AreEqual(9, children[9].Id); + } } - [TestCase("da-DK")] - [TestCase("en-US")] - public void FilterAvailable_MixedVariance_ForNonPreview_YieldsPublishedItemsInCultureOrInvariant(string culture) + [TestCase("da-DK", 4)] + [TestCase("en-US", 4)] + [TestCase("*", 5)] + public void FilterAvailable_MixedVariance_ForNonPreview_YieldsPublishedItemsInCultureOrInvariant(string culture, int expectedNumberOfChildren) { - var (sut, items) = SetupMixedVariance(false, culture); + var (sut, items) = SetupMixedVariance(false, culture == Constants.System.InvariantCulture ? "en-US" : culture); var children = sut.FilterAvailable(items.Keys, culture).ToArray(); - Assert.AreEqual(4, children.Length); + Assert.AreEqual(expectedNumberOfChildren, children.Length); // IDs 0 through 2 are invariant - only even IDs are published Assert.Multiple(() => @@ -140,16 +161,23 @@ public void FilterAvailable_MixedVariance_ForNonPreview_YieldsPublishedItemsInCu { Assert.AreEqual(8, children[3].Id); } + + if (culture == Constants.System.InvariantCulture) + { + Assert.AreEqual(6, children[3].Id); + Assert.AreEqual(8, children[4].Id); + } } - [TestCase("da-DK")] - [TestCase("en-US")] - public void FilterAvailable_MixedVariance_FoPreview_YieldsPublishedItemsInCultureOrInvariant(string culture) + [TestCase("da-DK", 8)] + [TestCase("en-US", 8)] + [TestCase("*", 10)] + public void FilterAvailable_MixedVariance_ForPreview_YieldsPublishedItemsInCultureOrInvariant(string culture, int expectedNumberOfChildren) { - var (sut, items) = SetupMixedVariance(true, culture); + var (sut, items) = SetupMixedVariance(true, culture == Constants.System.InvariantCulture ? "en-US" : culture); var children = sut.FilterAvailable(items.Keys, culture).ToArray(); - Assert.AreEqual(8, children.Length); + Assert.AreEqual(expectedNumberOfChildren, children.Length); // IDs 0 through 2 are invariant Assert.Multiple(() => @@ -180,6 +208,14 @@ public void FilterAvailable_MixedVariance_FoPreview_YieldsPublishedItemsInCultur Assert.AreEqual(8, children[6].Id); Assert.AreEqual(9, children[7].Id); } + + if (culture == Constants.System.InvariantCulture) + { + Assert.AreEqual(6, children[6].Id); + Assert.AreEqual(7, children[7].Id); + Assert.AreEqual(8, children[8].Id); + Assert.AreEqual(9, children[9].Id); + } } // sets up invariant test data: @@ -328,7 +364,7 @@ private IPublishStatusQueryService SetupPublishStatusQueryService(Dictionary items .TryGetValue(key, out var item) && idIsPublished(item.Id) - && (item.ContentType.VariesByCulture() is false || item.Cultures.ContainsKey(culture))); + && (culture == Constants.System.InvariantCulture || item.ContentType.VariesByCulture() is false || item.Cultures.ContainsKey(culture))); publishStatusQueryService .Setup(s => s.HasPublishedAncestorPath(It.IsAny())) .Returns(true); From bce85e1e88d95497f04dd2cb382f8f7c127a8dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 10 Nov 2025 10:38:53 +0100 Subject: [PATCH 126/155] Package section: use command icon for migrations, remove prop (#20775) change icon and remove ability to customize --- .../installed-packages-section-view-item.element.ts | 5 +---- .../installed/installed-packages-section-view.element.ts | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view-item.element.ts index ad05d682abd1..9d213a2f7230 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/views/installed/installed-packages-section-view-item.element.ts @@ -31,9 +31,6 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement { @property({ type: Boolean, attribute: false }) hasPendingMigrations = false; - @property({ attribute: 'custom-icon' }) - customIcon?: string; - @state() private _migrationButtonState?: UUIButtonState; @@ -123,7 +120,7 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement { version="${ifDefined(this.version ?? undefined)}" @open=${this.#onConfigure} ?readonly="${!this._packageView}"> - ${this.customIcon ? html`` : nothing} +
    ${this.hasPendingMigrations ? html` item.name, (item) => html` From 73fd52aeeac614af2a53416fba505c328fae194d Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Mon, 10 Nov 2025 11:21:33 +0100 Subject: [PATCH 127/155] Login: Added custom validation for missing password and user/email on the login form (#20233) * Added custom validation for missing password and user/email * Changed some of the logic behind custom validation, so it now uses aria-errormessage * fix: imports from src folder instead * build(deps-dev): bump vite to 7.2.0 * formatting * fix: moves the form into the login.page.element.ts component to better control submission * fix: creates elements globally * fix: adds id back to form * fix: no need to store references to all form elements * fix: errormessage should show with password field in a span as well * fix: checks validity of form * fix: constructs form in auth.element.ts anyway and append localization to validation and add oninput and onblur * chore: fixes import paths * fix: fixes special case where ?status was not reset * fix: changes wording in english * fix: removes duplicate en-us keys * feat: adds ariaLive and role attributes * fix: always clears the text * fix: username required validation should switch between username and email * package-lock.json updated on (re)install * Renamed SVG eye icon filenames to be conventional and kebab-cased. --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Co-authored-by: leekelleher --- src/Umbraco.Web.UI.Login/package-lock.json | 178 ++++++- src/Umbraco.Web.UI.Login/package.json | 52 +- .../assets/eye-closed.svg} | 0 .../openEye.svg => src/assets/eye-open.svg} | 0 src/Umbraco.Web.UI.Login/src/auth-styles.css | 40 +- src/Umbraco.Web.UI.Login/src/auth.element.ts | 163 +++--- .../back-to-login-button.element.ts | 4 +- .../components/pages/login.page.element.ts | 489 +++++++++--------- .../src/localization/lang/da.ts | 106 ++-- .../src/localization/lang/en-us.ts | 48 -- .../src/localization/lang/en.ts | 5 +- 11 files changed, 621 insertions(+), 464 deletions(-) rename src/Umbraco.Web.UI.Login/{public/closedEye.svg => src/assets/eye-closed.svg} (100%) rename src/Umbraco.Web.UI.Login/{public/openEye.svg => src/assets/eye-open.svg} (100%) diff --git a/src/Umbraco.Web.UI.Login/package-lock.json b/src/Umbraco.Web.UI.Login/package-lock.json index 6c509232c623..3b59e7e12450 100644 --- a/src/Umbraco.Web.UI.Login/package-lock.json +++ b/src/Umbraco.Web.UI.Login/package-lock.json @@ -10,7 +10,7 @@ "@umbraco-cms/backoffice": "^16.2.0", "msw": "^2.11.3", "typescript": "^5.9.3", - "vite": "^7.1.11" + "vite": "^7.2.0" }, "engines": { "node": ">=22", @@ -529,7 +529,6 @@ "integrity": "sha512-LSBHP2/wTF1BnaccHGX1t+0Ss+2VJQxotrLz/0+LK2z8ocuyVZXOYhfBSd7FP8sK78MDJVDBYrPCsBUvNSlH1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@hey-api/codegen-core": "^0.2.0", "@hey-api/json-schema-ref-parser": "1.2.0", @@ -644,7 +643,8 @@ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@lit/reactive-element": { "version": "2.1.1", @@ -652,6 +652,7 @@ "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@lit-labs/ssr-dom-shim": "^1.4.0" } @@ -704,7 +705,8 @@ "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.47.1", @@ -1007,6 +1009,7 @@ "integrity": "sha512-viQ6AHRhjCYYipKK6ZepBzwZpkuMvO9yhRHeUZDvlSOAh8rvsUTSre0y74nu8QRYUt4a44lJJ6BpphJK7bEgYA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1021,6 +1024,7 @@ "integrity": "sha512-zCce9PRuTNhadFir71luLo99HERDpGJ0EEflGm7RN8I1SnNi9gD5ooK42BOIQtejGCJqg3hTPZiYDJC2hXvckQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1035,6 +1039,7 @@ "integrity": "sha512-HHakuV4ckYCDOnBbne088FvCEP4YICw+wgPBz/V2dfpiFYQ4WzT0LPK9s7OFMCN+ROraoug+1ryN1Z1KdIgujQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1065,6 +1070,7 @@ "integrity": "sha512-GU9deB1A/Tr4FMPu71CvlcjGKwRhGYz60wQ8m4aM+ELZcVIcZRa1ebR8bExRIEWnvRztQuyRiCQzw2N0xQJ1QQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1079,6 +1085,7 @@ "integrity": "sha512-/TDDOwONl0qEUc4+B6V9NnWtSjz95eg7/8uCb8Y8iRbGvI9vT4/znRKofFxstvKmW4URu/H74/g0ywV57h0B+A==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1094,6 +1101,7 @@ "integrity": "sha512-2P2IZp1NRAE+21mRuFBiP3X2WKfZ6kUC23NJKpn8bcOamY3obYqCt0ltGPhE4eR8n8QAl2fI/3jIgjR07dC8ow==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1108,6 +1116,7 @@ "integrity": "sha512-JkDQU2ZYFOuT5mNYb8OiWGwD1HcjbtmX8tLNugQbToECmz9WvVPqJmn7V/q8VGpP81iEECz/IsyRmuf2kSD4uA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1123,6 +1132,7 @@ "integrity": "sha512-KOiMZc3PwJS3hR0nSq5d0TJi2jkNZkLZElcT6pCEnhRHzPH6dRMu9GM5Jj798ZRUy0T9UFcKJalFZaDxnmRnpg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1138,6 +1148,7 @@ "integrity": "sha512-d6uStdNKi8kjPlHAyO59M6KGWATNwhLCD7dng0NXfwGndc22fthzIk/6j9F6ltQx30huy5qQram6j3JXwNACoA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1152,6 +1163,7 @@ "integrity": "sha512-KSzL8WZV3pjJG9ke4RaU70+B5UlYR2S6olNt5UCAawM+fi11mobVztiBoC19xtpSVqIXC1AmXOqUgnuSvmE4ZA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1166,6 +1178,7 @@ "integrity": "sha512-m6YR1gkkauIDo3PRl0gP+7Oc4n5OqDzcjVh6LvWREmZP8nmi94hfseYbqOXUb6RPHIc0JKF02eiRifT4MSd2nw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1181,6 +1194,7 @@ "integrity": "sha512-mT6baqOhs/NakgrAeDeed194E/ZJFGL692H0C7f1N7WDRaWxUu2oR0LrnRqSH5OyPjELkzu6nQnNy0+0tFGHHg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1211,6 +1225,7 @@ "integrity": "sha512-pOs6oU4LyGO89IrYE4jbE8ZYsPwMMIiKkYfXcfeD9NtpGNBnjeVXXF5I9ndY2ANrCAgC8k58C3/powDRf0T2yA==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1244,6 +1259,7 @@ "integrity": "sha512-quOXckC73Luc3x+Dcm88YAEBW+Crh3x5uvtQOQtn2GEG91AshrvbnhGRiYnfvEN7UhWIS+FYI5liHFcRKSUKrQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1258,6 +1274,7 @@ "integrity": "sha512-UHKNRxq6TBnXMGFSq91knD6QaHsyyOwLOsXMzupmKM5Su0s+CRXEjfav3qKlbb9e4m7D7S/a0aPm8nC9KIXNhQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1272,6 +1289,7 @@ "integrity": "sha512-UezvM9VDRAVJlX1tykgHWSD1g3MKfVMWWZ+Tg+PE4+kizOwoYkRWznVPgCAxjmyHajxpCKRXgqTZkOxjJ9Kjzg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1302,6 +1320,7 @@ "integrity": "sha512-CkoRH+pAi6MgdCh7K0cVZl4N2uR4pZdabXAnFSoLZRSg6imLvEUmWHfSi1dl3Z7JOvd3a4yZ4NxerQn5MWbJ7g==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1407,6 +1426,7 @@ "integrity": "sha512-p2n8WVMd/2vckdJlol24acaTDIZAhI7qle5cM75bn01sOEZoFlSw6SwINOULrUCzNJsYb43qrLEibZb4j2LeQw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1436,6 +1456,7 @@ "integrity": "sha512-t9Nc/UkrbCfnSHEUi1gvUQ2ZPzvfdYFT5TExoV2DTiUCkhG6+mecT5bTVFGW3QkPmbToL+nFhGn4ZRMDD0SP3Q==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -1560,7 +1581,8 @@ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/markdown-it": { "version": "14.1.2", @@ -1568,6 +1590,7 @@ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -1578,7 +1601,8 @@ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/statuses": { "version": "2.0.5", @@ -1592,7 +1616,8 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@umbraco-cms/backoffice": { "version": "16.2.0", @@ -1734,6 +1759,7 @@ "integrity": "sha512-O0807+bWVWV/rsFihFVKSOkg9wBtLXKCszE5+eZk2KmONm93BFhIAE35rp7eD6X2SuJMHwYzInIxMIMjHzdpUQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-button-group": "1.15.0" @@ -1745,6 +1771,7 @@ "integrity": "sha512-eEX83zwRN3tCiHScKcmkROWAfLu3TgFI9SntlbyxiuSSYfhJxWSZXOf6lVvQ/1CyvKq8XqSbBnN3VKXgcaKOpg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -1755,6 +1782,7 @@ "integrity": "sha512-CGYAFAHgNoQK2UTupP7pO0mwP6t9/Ms6WZ0gIC40a+kPjrGtaDWU52hiPsuXrUcR6WjJwZ5WRrJHOboRpdmM0g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-avatar": "1.15.0", "@umbraco-ui/uui-base": "1.15.0" @@ -1766,6 +1794,7 @@ "integrity": "sha512-9aGmhRvey98kcR7wfsnno0BNftIRwJ0r2lCH6cNK2lhe69enYm0MWjp+4uutnlEWWskTLEI344BOqmqOHH1NPA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -1776,6 +1805,7 @@ "integrity": "sha512-0vtKmjzUOn/tIUHNrsx7aZpy3eq9aRKqV9kkJTrhH92S4WcMy+cOB1iw9t3Fe3xlBPuL3JpszwuxMTIuIqJTgQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "lit": ">=2.8.0" } @@ -1786,6 +1816,7 @@ "integrity": "sha512-LkYX+p44mFVdvlZSliP5ClMcyHoOIVLWI3WVkaMLQdNi2LO9bbfaTneLzu4ruW6v0iF+kLsznr3bl73VHk7oEg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -1796,6 +1827,7 @@ "integrity": "sha512-MhSNcKsVNymD/yt3NFXujuaQmAqMqj5S+CBoDHEk88H7Id9NMw9RStZFJ37mI2CxHWkeHDotNVgOhSBiHJNJnw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-css": "1.15.0" @@ -1807,6 +1839,7 @@ "integrity": "sha512-TaUY+hNB0VIwv9SBi9fDjIFRtrmmkcT7hlhLCJLUVfQ7jJlGLPISAAdypSplNeCPthYvP1cJQ9m28OzscXHZxQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -1817,6 +1850,7 @@ "integrity": "sha512-3Oaqj6Yta/Q/Ndme20YA1XbHdBBL71iNhpqREfTHli2YV4TEcgIiNy0s2op2oPhKjIEQPEfitU2BrruYEEWa7Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-icon-registry-essential": "1.15.0" @@ -1828,6 +1862,7 @@ "integrity": "sha512-MAaJzpwVnlyGJNvLv6qIwrYsI5SaXXiVKgVi47I8+x//QmnArmetCN04766gGzmAb0m2uuC3ht0BXMDv05pxvw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-button": "1.15.0" @@ -1839,6 +1874,7 @@ "integrity": "sha512-YPEnubKNbKmw04eWRH24/3Uxu+zhtLPeJoaT6ykPCyjr/EKc82rSTvn8fwQuP41UokQrXOac2pKn7SncyoST1Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -1849,6 +1885,7 @@ "integrity": "sha512-nLJZ6P5eK1rYgqjP5zCxbZp8g4WJ23RnUZQ49o7QpU/7zoPOK72/fuM3Ky00Iapixm/kAD6dYHO/P+GtNz8/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -1859,6 +1896,7 @@ "integrity": "sha512-pNjpk2iIdSsmTtDdBsWaEr8JX0RcWbl8yKGaqLvo/S7d3bly5z+FjcsgGnX1i1GHo7dqmgVJfbdvN9V1jgn+FA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-checkbox": "1.15.0" @@ -1870,6 +1908,7 @@ "integrity": "sha512-cWag+D0XrogYZesAN8NMPQCCuU7L7uZ4Xz8dmirKQk1gjMrFDC4vYPZRQ/5O3ETTFupfDipVKimgRDsmarbLSQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-card": "1.15.0" @@ -1881,6 +1920,7 @@ "integrity": "sha512-DZ6JYNvGb5wVkhhLShENMm+Y6kTpz37YrApQTJVUUgPXhIABO2CDCnqgpH5tkQX73s9jjVB3Ca7SeivuYv8G9A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-card": "1.15.0", @@ -1893,6 +1933,7 @@ "integrity": "sha512-EzYebWCzR0wHY902NmAcTRSVSscYac3QntCz+xwSulrhzfy4copeOd1qE+Lz7FjHs+ho0IBPZol8sF4W6rK8FQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-card": "1.15.0", @@ -1906,6 +1947,7 @@ "integrity": "sha512-oo7gCs3RGJ4ujFs+LpG9I1DS/XSNkz9gaqvp4BkaR0kBXzw5f2SLLGhA9S3M6M+OPlsXuuJNKlTV1tn2+LF6Ng==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-avatar": "1.15.0", "@umbraco-ui/uui-base": "1.15.0", @@ -1918,6 +1960,7 @@ "integrity": "sha512-cnKP5GeaI028hGabVCki1kPqAVSekFeP7QEwu7lncA+dcX8uvg+ffV6kW9FV0emOhI5Tmxvh8o+UDKlLs28q3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -1928,6 +1971,7 @@ "integrity": "sha512-vPkgrFAPDMvJdJTADIWNj48D8gJWD3dBotucUghg/wHhvJv8h/2MvnwMUNnnSB1REHbanl7hJBVvKcNkoil0gA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-boolean-input": "1.15.0", @@ -1940,6 +1984,7 @@ "integrity": "sha512-k6u//b+s6UYmzKYMizIf2MRGD4kFy1qWdSk1GnIeDdiQxABJuBZEtkACIe66j+lxnonFvZ/assbLbhRiu15ZCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "colord": "^2.9.3" @@ -1951,6 +1996,7 @@ "integrity": "sha512-I4KGyzZZazjeifcavHp7qnMbP0Jh0dM+gzZhV+YtdPR2JT0o7y6stkbY0f+dOln0K6Bu6BQLV0HLHl/1f/1NDg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-popover-container": "1.15.0", @@ -1963,6 +2009,7 @@ "integrity": "sha512-lpT9kapypGkTelG9COSk169VKs0MSiKweX8menDDn0p6I4RfKQBy0N27HecCcf1RqPsCnTbP3lPr5DJy00KdzA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -1973,6 +2020,7 @@ "integrity": "sha512-1AI0QMr046fKc8xZ4aBO7FDwvggsS9plIpY0W4AGrqQxqGUR2u/mTU49+8xMtboaFOen5RQpJ65DN9hAgeNZ+w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-icon-registry-essential": "1.15.0", @@ -1985,6 +2033,7 @@ "integrity": "sha512-UzlgWdsVHyCM/znFThrfA4A/S/K/R9Nc2KyRYiyy2xgBoP7x2vJ5Rn4mnR02W4bhI3gNgCJ2fqhmyiW4dxyk0Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-color-swatch": "1.15.0" @@ -1996,6 +2045,7 @@ "integrity": "sha512-CKslvVRCKCReMr/ZZh4wc3TKJNvFjKVm/hSIvFqCIoJuSKfC4XuLU9SK9FL1s42NUAUmccSD3hATZJZ9VXqY+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-button": "1.15.0", @@ -2012,6 +2062,7 @@ "integrity": "sha512-e8IhqU9AC5pOqXuzPzI+BDsW97Ac0u1GU/5MIJqRcBZ+ZttPcH4fsm4u8siCHbK1khCG0Vzo7HiKPZ0IuuOslg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2033,6 +2084,7 @@ "integrity": "sha512-iVsrVVnvBrCCT9uJhyBE7b1kXwWUUKDmimhs/TyF1SFjxWP/U0Z99QqqI1pawdad+BuK3oVCmxYOdaReWDQXkQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-css": "1.15.0" @@ -2044,6 +2096,7 @@ "integrity": "sha512-JdDRIzSGGDnvVqXSIhc+5rDXMdYMO+Hd7s2hqLp+iRSn8IHISN/qT1nfFVO9LMbLdcApanl3JJ4Rru9LN4Q3HA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2054,6 +2107,7 @@ "integrity": "sha512-MhJRkVdDQWKEBvemNRD4bZCuIS0JUll1nNoPK7scA+e6vDmbv25vqPHNXGE/sIpVkChY/L+v+twokzlHn57XMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-symbol-file-dropzone": "1.15.0" @@ -2065,6 +2119,7 @@ "integrity": "sha512-AHKIdYLC0ga4Wgr68xtW/gG3NDqn+QhD2aus0l2n4lBoq6OAQ5aZiPwD9i1fCD7dgyjKQ6Ov9PJSaqRYQkOlNA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-symbol-file": "1.15.0", @@ -2078,6 +2133,7 @@ "integrity": "sha512-4u9ZryfVBunpb0IL0+TevytrISA6S1+AajiK/PUk0JMJfqMuQMjmpnNPdtYRNVgFFIcQFQKipjT/mrHbDVawxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2088,6 +2144,7 @@ "integrity": "sha512-fiWGeQpREnl6k+6VNHz9ixNdEmOoFNm7qsgdIYJ1jCDXBGME1mjxJOr2Eq7UWJuzQM8BeyQEcXq5SVIOv21PRw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-form-validation-message": "1.15.0" @@ -2099,6 +2156,7 @@ "integrity": "sha512-RYfwmjPkY0KumjaalLW8gkasW25Mj87YFAzJn7mAYiZigURape9RqGpvrBfwcMmGj3W2/uVuHxpAHrvweQOt4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2109,6 +2167,7 @@ "integrity": "sha512-e8/W6gu6kwrodH0f0U70LR5rHQhiZGq3NqLqikAQ1rvmwitXUqtkVXIhkGxSf7M6yPhpmoi2qEEZDQH9cvCE5A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2119,6 +2178,7 @@ "integrity": "sha512-nIdzCqoABeRVG6jW045ok649MiAhm5zPdfuMKc1a+TNw9xkKj+vnA1YcjaBN502+AekMhhwnqgj9mLL+mC2VPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-icon": "1.15.0" @@ -2130,6 +2190,7 @@ "integrity": "sha512-llHFVMlV3Uyg2fHiNt1qfDgRhLthD37uQD2FzlQb0GEYjp+4dE8Jyc/eZW2mqABPweUJACVwbrwBUVrCeQJ1OQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-icon-registry": "1.15.0" @@ -2141,6 +2202,7 @@ "integrity": "sha512-vPc4I/kkQM9RWfHI0F/OQhoTu+KefplbQp0JEQ4gfr6MwxIm6bBTEuw8T5K9t1DQs8EZ7yeLEsSh65FDPepRtg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2151,6 +2213,7 @@ "integrity": "sha512-VVn2FMsflvEWd6fOX0HQ3JaUh7haznqSqCLTSTOduh/H3jE+dVYCW6YC5uTsxArmOwsSBYSfBQNetW23eJim3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-action-bar": "1.15.0", "@umbraco-ui/uui-base": "1.15.0", @@ -2166,6 +2229,7 @@ "integrity": "sha512-AFyVYNeExHXe10b3/5/BLZOmMKyMxzftsO0HKbaQQuxrxL2SCHsQJRUpxSY+/0vAl2JbNdmrk0HTsP1O4Y9zig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-button": "1.15.0", @@ -2179,6 +2243,7 @@ "integrity": "sha512-Pe8lNdHz/6IfbQyWEi0o+pKJ6/zunQ2b8HARCU0a9HFXRDk+XsAuBsn79zQXZl5MvseAUQrnouLwPHpdtMbeMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-icon-registry-essential": "1.15.0", @@ -2191,6 +2256,7 @@ "integrity": "sha512-8Q/G5Lg6949BbMHQ1BhZ9UpoJjOQ19w1tl2y0d/rP3w/mKnTQaBSf+MQmA/6kQ/Unb2wHXJANr4pAGpUklOg6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2201,6 +2267,7 @@ "integrity": "sha512-fnmRl+RGUROERvt+Jw0WiW3Btlddg0Xka6F+gR95gy5gr/v8s34uf1/bbPD3hWUXZPukLmxeMjbzyuqMrO8rpQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2211,6 +2278,7 @@ "integrity": "sha512-HQ2zCp2kz45GWQ3wV153ytuYD2KcdeAA5RRUVrN0Zn3GQB3wfG7xMkQQNRAOWMUdnfqmdQHeK+COO7NaET3VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2221,6 +2289,7 @@ "integrity": "sha512-4eMeerunFc5yZsJIwpHADn8oGcu0Nn36oyKbSd0qC0mNmmN2i8UOF9w4O+lndd2L0Mhv23FGvBRo7mb5EAvWlg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2231,6 +2300,7 @@ "integrity": "sha512-4rG8UHvyS2qvsjQYEmYjKX01SRwfk60oH8SSSx8r3z2BM62dCOa+4SBhLxqiBciC/u8FtN8X20MIGE0+eMdtoA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2241,6 +2311,7 @@ "integrity": "sha512-BOebCMB/p4TaK4kJYrYgimC6SSGBHN4y1MytK3tyvObbuj3gVqkbwHW5CZrhK4jMaywRgGq96OsuaGfc52HFog==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-loader-bar": "1.15.0", @@ -2253,6 +2324,7 @@ "integrity": "sha512-EDz1Qx+mTXNvOu565IculPCyuuFHwBon2wYnfWDBMoHJ5+P54jBHzg2U/8fUVse7xKPvU21hF4JneAvycNIiGQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2263,6 +2335,7 @@ "integrity": "sha512-sPVs1bApKupNd2JcSMwFS1060Y++Fti1ybJrafcLh1+h4IjmLDIRTHTTL8C+kei5G2Oi3+Z7vGpLu7lrTAmRlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-button": "1.15.0", @@ -2275,6 +2348,7 @@ "integrity": "sha512-VCHVvO0fd5eL5UvB/RPL/K68UhOgsIpuyr+aXLblaYT/6at2LNosUxR4eRW2r0WOQzOiyE+Nu69pExBKyfT8bw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2285,6 +2359,7 @@ "integrity": "sha512-54M4G0ru8j5ltPAdDGIxogdmos33hxeQeusI/uMFxo2yqHHyMHRi95vvCdcwFmGlEdFd2rsnxZKfNMUKM99GKQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2295,6 +2370,7 @@ "integrity": "sha512-vtGUwHaG4EDLQERkwym51OasoWLj30LQLhcCCYXDJtTL1dG2nIKScEtlSUiVh5qRsI+V+kaBYPGD4TFD5o43tQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2305,6 +2381,7 @@ "integrity": "sha512-5TUF/iWUzbVXvBs9Z273q6s9yLbns8itTiFHCITw5w5fZzDn8R6O5hrOW7tV79kCxAnBSOAVP8v1JhGTwXw19Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2315,6 +2392,7 @@ "integrity": "sha512-HMHVdBoB1O39rojofezee2aXGv6CMn7dUFvNefdF9HxmNrIcpFBYXSL8aBt5QJeziFQMwbCtqyY21aUag0nzfw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2325,6 +2403,7 @@ "integrity": "sha512-w7FZIe5mtsgvsf6hOH5mHKDBzg9Rd/+viyk/xNVs1NeZBn1nWEIHZs0R7YMpv+QxulklhAOpBcbGoUTB8xE+vA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2335,6 +2414,7 @@ "integrity": "sha512-UT65bpUmRlEgVuvn2RlTZ5l2WDF82jH1t8g+6HV6OJctpqTKlOfPkQmd6AluESPhHFEwwTydS/M7x+X3Adkdtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2345,6 +2425,7 @@ "integrity": "sha512-ybDqIt1cXd7AiZLZsDrSHCMp2zM8I+0lmN599b3NROjm59SZXIvpbY1TS1gJ45htgsc18x2y+S4laInYu2dGUg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-icon": "1.15.0", @@ -2357,6 +2438,7 @@ "integrity": "sha512-59s16558ySCX7b9IT/Sorq0fdFeCRENSTa7DIkQUrvVPaFWqKFz9jCYFEqDnH11jZHGsNiYh5YCmWlF/VNbwJQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-ref-node": "1.15.0" @@ -2368,6 +2450,7 @@ "integrity": "sha512-AWPZPkFGcAkRx4j6JnOi2r3EJxnvZUXfhOWNyWB2/dFRckatPH56+lVkqV+fRC+81emKBSQWkx2NphFzLEMr0A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-ref-node": "1.15.0" @@ -2379,6 +2462,7 @@ "integrity": "sha512-knIIbbfoWtepOvyC54dCo3xF0Vuap6i5uMQPd+wITCmg56a+yiJFuke+dyzatOIeXzgspLgFUngwQZEj5mJnWA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-ref-node": "1.15.0" @@ -2390,6 +2474,7 @@ "integrity": "sha512-pXvL523m2JR3P8OO+E1AE4YAaYhJLc519CtjNXSuctNIk1yWvwxBu8VozLIQV+xrOXGz+SiXwDkoaRPwjTQKtg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-ref-node": "1.15.0" @@ -2401,6 +2486,7 @@ "integrity": "sha512-bQWfZPKJyAf2O/YvOD6fVSSpKaYZMBsrEGT+ydLPv3BNJroYHS8+NEbulZxExWztNApTcs6Vo5T19AUz+vsnLQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-ref-node": "1.15.0" @@ -2412,6 +2498,7 @@ "integrity": "sha512-4GpRzhGedMwjqW1Wk7AvgakNCc6S1edYpHWeO6cfmryIm0hvnCfkU132lzLmB+Ag2QIOI8p4Ak0OQHYWd+XZHw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-ref-node": "1.15.0" @@ -2423,6 +2510,7 @@ "integrity": "sha512-L4qM6GPDqu0/9B2OVb3EljZT3zYxbwp6uOz1nfVYpGAWBxd6INOtNbn8WYdZLq6qqa8NR6qK+su2554nInvQGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2433,6 +2521,7 @@ "integrity": "sha512-yRx+TlXBB05jM8ShThRooFgCS5nSN7eAAnpttZgBWqY3sccIVy2Knbkz3kXLJE6ld+bO5nUXhsZBZ34MEHkiog==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2443,6 +2532,7 @@ "integrity": "sha512-+OAzOutyUB2WCI+e5aFRoUNsFFuc/hUXnpIjx4P1moOYiggc/NxjaTHz5mxbmkC11yyS+0vpl8lVSZglkLCH5w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2453,6 +2543,7 @@ "integrity": "sha512-6y9rpFfhtuWMnaAamlzrB5Q12dsZ8dprmQaGtKr+g97PTNRPC3/dc5sdROam8VMDAhL9MkfBAZBoS6yAoJsPcQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2463,6 +2554,7 @@ "integrity": "sha512-F0BueWxu6J5P7xyzkp1c/eFZJjStsw65hB3bNEmWBOqkm/jbBKg9+Xs99tov+VwCHYOt8T+DuEDkkKmdxVAxyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2473,6 +2565,7 @@ "integrity": "sha512-D5DottbukIFxL+YTVEMujHPdqB8Hhw02TKpegfDQb8UGSPC5pCQw4O212TSuyTalKb598niNmCzcjEG5TWNqww==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2483,6 +2576,7 @@ "integrity": "sha512-JLcEVnJqv2LL8VtscPCtPKda9ywWzV4vd0XODHLE3iI1cgHeNwMBhxqgkah0ULuw5w2Hrq8gwQ7/DuPHMSIFxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2493,6 +2587,7 @@ "integrity": "sha512-CPsr1K5IkdxBgID+xoIcgbumm/z0q+Z/1NPxTO40EL7kx3KOLQ8vwLdOTSW1cTj90JFA9+XuRtOpmMEY0XjICg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2503,6 +2598,7 @@ "integrity": "sha512-5QyDNFjiBeuPgalT9zwPMP220zJUHPpbPvCohWCFLn/2JJsa6IjSMtsAcqxI154ZJ9vYX7vYiYUn8tJTY8CHpA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2513,6 +2609,7 @@ "integrity": "sha512-BQq7BwZ7nCcgKE5tMhG6OVYTrrMEIXpx8kQKec/ULgVfs0/Ws6qeH9u4rGVK/yHU8gecd6DSeUczjjq2iS4djA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2523,6 +2620,7 @@ "integrity": "sha512-5Akw8T0SV2OrwvPk1JSeFr1clvHE4N0DwceSU9bn9f6gLIGGRxvniJAclQDRI/Woe3hm8waMy6cC2fXfSdc6lg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2533,6 +2631,7 @@ "integrity": "sha512-AnPp0QJeI70ucX8ludr3qaFmlxjKZUarX10DI8ieIB8VJiQZo0TjoPcPdSGmZupaPBLiszlpb3rKzGkZhXEIHg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2543,6 +2642,7 @@ "integrity": "sha512-oS0eA5Z8+s+5o2ks3WCED5VGP8AunRLyuB2y7kVdRUfhCfck7B9v83zNfxPVoGoVsTDLtAQM1S4P8SHwNRmk7g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-button": "1.15.0", @@ -2556,6 +2656,7 @@ "integrity": "sha512-PgyZvAiOZmXmiRW4UhfD6Tybx3ft755aKAVqT8ELpskLSvVr1oz4uTI6+QxoeQ1AkrHovenvIdBX+Iwi91SheQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2566,6 +2667,7 @@ "integrity": "sha512-tk/RVzCxs+KPSJ+qH2Xlr9RYxcdrSNulDKk5sBCQR0A9nwOffa15SGreSMKWgq+gYOVYChHBg/WxLWLq3d7Rlg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2576,6 +2678,7 @@ "integrity": "sha512-nz+snpjPFE+ftH5R/ekgZYy9ofGAf51yQYjWCtBwkrQ6D1dIBYA6kynZFdqIefrRwJJ5zHpe25BcS/AyRPc/9Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-button": "1.15.0", @@ -2590,6 +2693,7 @@ "integrity": "sha512-fd5d0DU/x2+u15rP0wrjw29M0oqsDFmnAfbPEdgQoPV+hvq9/SLhxJtzx10ZSNXoyuO9sTK50Q7nYsqOvGCzqg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-toast-notification": "1.15.0" @@ -2601,6 +2705,7 @@ "integrity": "sha512-uf/e/dVN6kqX76vcawiQM3w1nMHa8A+ZTtNwxtmAZi8bNPwjXLNaqKfeSp2thTByCIzFz7imnft56QtYLbksOA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-css": "1.15.0" @@ -2612,6 +2717,7 @@ "integrity": "sha512-WLooENcxuAobbXxN1W2uKGh/cN9k0f3cRmDDtCZdgjeheGlBYWatkc5HQte7zchXHUi0xTrsvBCBa9CsLKN/3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0", "@umbraco-ui/uui-boolean-input": "1.15.0" @@ -2623,6 +2729,7 @@ "integrity": "sha512-vn3dbpYGekAqG944Vkwd0ILQRtTaZtL1BVdsge2UsU8sOsEKwv5YzQal4b+o8yu8nb4vZbWHZ2zRmnpnPgPmjg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@umbraco-ui/uui-base": "1.15.0" } @@ -2848,7 +2955,8 @@ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/commander": { "version": "13.0.0", @@ -2892,7 +3000,8 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/default-browser": { "version": "5.2.1", @@ -3007,6 +3116,7 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=0.12" }, @@ -3071,6 +3181,7 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -3282,6 +3393,7 @@ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "uc.micro": "^2.0.0" } @@ -3291,7 +3403,8 @@ "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lit": { "version": "3.3.1", @@ -3312,6 +3425,7 @@ "integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@lit-labs/ssr-dom-shim": "^1.4.0", "@lit/reactive-element": "^2.1.0", @@ -3324,6 +3438,7 @@ "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@types/trusted-types": "^2.0.2" } @@ -3341,6 +3456,7 @@ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -3372,7 +3488,8 @@ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/minimist": { "version": "1.2.8", @@ -3532,7 +3649,8 @@ "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/outvariant": { "version": "1.4.3", @@ -3575,7 +3693,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3630,6 +3747,7 @@ "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-transform": "^1.0.0" } @@ -3640,6 +3758,7 @@ "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-state": "^1.0.0" } @@ -3650,6 +3769,7 @@ "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", @@ -3662,6 +3782,7 @@ "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", @@ -3674,6 +3795,7 @@ "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", @@ -3687,6 +3809,7 @@ "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", @@ -3700,6 +3823,7 @@ "integrity": "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" @@ -3711,6 +3835,7 @@ "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" @@ -3722,6 +3847,7 @@ "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", @@ -3734,6 +3860,7 @@ "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", @@ -3758,6 +3885,7 @@ "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.25.0" } @@ -3768,6 +3896,7 @@ "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", @@ -3793,6 +3922,7 @@ "integrity": "sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-keymap": "^1.2.2", "prosemirror-model": "^1.25.0", @@ -3807,6 +3937,7 @@ "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" @@ -3823,6 +3954,7 @@ "integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.21.0" } @@ -3846,6 +3978,7 @@ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -3937,7 +4070,8 @@ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/run-applescript": { "version": "7.0.0", @@ -4116,7 +4250,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/type-fest": { "version": "4.34.1", @@ -4137,7 +4272,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4151,7 +4285,8 @@ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/uglify-js": { "version": "3.19.3", @@ -4193,9 +4328,9 @@ } }, "node_modules/vite": { - "version": "7.1.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", - "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.0.tgz", + "integrity": "sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w==", "dev": true, "license": "MIT", "dependencies": { @@ -4272,7 +4407,8 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/wordwrap": { "version": "1.0.0", diff --git a/src/Umbraco.Web.UI.Login/package.json b/src/Umbraco.Web.UI.Login/package.json index c0f863dc50f8..47809d0d1496 100644 --- a/src/Umbraco.Web.UI.Login/package.json +++ b/src/Umbraco.Web.UI.Login/package.json @@ -1,28 +1,28 @@ { - "name": "login", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "watch": "tsc && vite build --watch", - "preview": "vite preview", - "generate:server-api": "openapi-ts" - }, - "engines": { - "node": ">=22", - "npm": ">=10.9" - }, - "devDependencies": { - "@hey-api/openapi-ts": "^0.85.0", - "@umbraco-cms/backoffice": "^16.2.0", - "msw": "^2.11.3", - "typescript": "^5.9.3", - "vite": "^7.1.11" - }, - "msw": { - "workerDirectory": [ - "public" - ] - } + "name": "login", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "watch": "tsc && vite build --watch", + "preview": "vite preview", + "generate:server-api": "openapi-ts" + }, + "engines": { + "node": ">=22", + "npm": ">=10.9" + }, + "devDependencies": { + "@hey-api/openapi-ts": "^0.85.0", + "@umbraco-cms/backoffice": "^16.2.0", + "msw": "^2.11.3", + "typescript": "^5.9.3", + "vite": "^7.2.0" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } } diff --git a/src/Umbraco.Web.UI.Login/public/closedEye.svg b/src/Umbraco.Web.UI.Login/src/assets/eye-closed.svg similarity index 100% rename from src/Umbraco.Web.UI.Login/public/closedEye.svg rename to src/Umbraco.Web.UI.Login/src/assets/eye-closed.svg diff --git a/src/Umbraco.Web.UI.Login/public/openEye.svg b/src/Umbraco.Web.UI.Login/src/assets/eye-open.svg similarity index 100% rename from src/Umbraco.Web.UI.Login/public/openEye.svg rename to src/Umbraco.Web.UI.Login/src/assets/eye-open.svg diff --git a/src/Umbraco.Web.UI.Login/src/auth-styles.css b/src/Umbraco.Web.UI.Login/src/auth-styles.css index 9ea9111fc88a..76c38bc09112 100644 --- a/src/Umbraco.Web.UI.Login/src/auth-styles.css +++ b/src/Umbraco.Web.UI.Login/src/auth-styles.css @@ -1,4 +1,19 @@ -#umb-login-form #username-input { +.errormessage { + color: var(--uui-color-invalid-standalone); + display: none; + margin-top: var(--uui-size-1); +} + +.errormessage.active { + display: block; +} + +uui-form-layout-item { + margin-top: var(--uui-size-space-4); + margin-bottom: var(--uui-size-space-4); +} + +#username-input { width: 100%; height: var(--input-height); box-sizing: border-box; @@ -9,21 +24,16 @@ padding: var(--uui-size-1, 3px) var(--uui-size-space-4, 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 #username-input:focus-within { +#username-input:focus-within { border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1)); outline: calc(2px * var(--uui-show-focus-outline, 1)) solid var(--uui-color-focus); } -#umb-login-form #username-input:hover:not(:focus-within) { +#username-input:hover:not(:focus-within) { border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2)); } -#umb-login-form #password-input-span button { +#password-show-toggle { color: var(--uui-color-default-standalone); display: inline-flex; justify-content: center; @@ -39,12 +49,12 @@ transition-timing-function: linear; } -#umb-login-form #password-input-span button:hover { +#password-show-toggle:hover { color: var(--uui-color-default-emphasis); cursor: pointer; } -#umb-login-form #password-input-span { +#password-input-span { display: inline-flex; width: 100%; align-items: center; @@ -60,7 +70,7 @@ padding: var(--uui-size-1, 3px) var(--uui-size-space-4, 9px); } -#umb-login-form #password-input-span input { +#password-input { flex-grow: 1; align-self: stretch; min-width: 0; @@ -70,15 +80,15 @@ outline-style: none; } -#umb-login-form #password-input-span:focus-within { +#password-input-span:focus-within { border-color: var(--uui-input-border-color-focus, var(--uui-color-border-emphasis, #a1a1a1)); outline: calc(2px * var(--uui-show-focus-outline, 1)) solid var(--uui-color-focus); } -#umb-login-form #password-input-span:hover:not(:focus-within) { +#password-input-span:hover:not(:focus-within) { border-color: var(--uui-input-border-color-hover, var(--uui-color-border-standalone, #c2c2c2)); } -#umb-login-form input::-ms-reveal { +#password-input::-ms-reveal { display: none; } diff --git a/src/Umbraco.Web.UI.Login/src/auth.element.ts b/src/Umbraco.Web.UI.Login/src/auth.element.ts index d574ef460186..a3ba757cb025 100644 --- a/src/Umbraco.Web.UI.Login/src/auth.element.ts +++ b/src/Umbraco.Web.UI.Login/src/auth.element.ts @@ -3,15 +3,15 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { InputType, UUIFormLayoutItemElement } from '@umbraco-cms/backoffice/external/uui'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { UMB_AUTH_CONTEXT, UmbAuthContext } from './contexts'; -import { UmbSlimBackofficeController } from './controllers'; +import { UMB_AUTH_CONTEXT, UmbAuthContext } from './contexts/index.js'; +import { UmbSlimBackofficeController } from './controllers/index.js'; // We import the authStyles here so that we can inline it in the shadow DOM that is created outside of the UmbAuthElement. import authStyles from './auth-styles.css?inline'; // Import the SVG files -import openEyeSVG from '../public/openEye.svg?raw'; -import closedEyeSVG from '../public/closedEye.svg?raw'; +import svgEyeOpen from './assets/eye-open.svg?raw'; +import svgEyeClosed from './assets/eye-closed.svg?raw'; // Import the main bundle import { extensions } from './umbraco-package.js'; @@ -21,6 +21,7 @@ const createInput = (opts: { type: InputType; name: string; autocomplete: AutoFill; + errorId: string; inputmode: string; autofocus?: boolean; }) => { @@ -31,7 +32,10 @@ const createInput = (opts: { input.id = opts.id; input.required = true; input.inputMode = opts.inputmode; + input.setAttribute('aria-errormessage', opts.errorId); input.autofocus = opts.autofocus || false; + input.className = 'input'; + return input; }; @@ -46,6 +50,14 @@ const createLabel = (opts: { forId: string; localizeAlias: string; localizeFallb return label; }; +const createValidationMessage = (errorId: string) => { + const validationElement = document.createElement('div'); + validationElement.className = 'errormessage'; + validationElement.id = errorId; + validationElement.role = 'alert'; + return validationElement; +}; + const createShowPasswordToggleButton = (opts: { id: string; name: string; @@ -58,7 +70,7 @@ const createShowPasswordToggleButton = (opts: { button.name = opts.name; button.type = 'button'; - button.innerHTML = openEyeSVG; + button.innerHTML = svgEyeOpen; button.onclick = () => { const passwordInput = document.getElementById('password-input') as HTMLInputElement; @@ -66,11 +78,11 @@ const createShowPasswordToggleButton = (opts: { if (passwordInput.type === 'password') { passwordInput.type = 'text'; button.ariaLabel = opts.ariaLabelHidePassword; - button.innerHTML = closedEyeSVG; + button.innerHTML = svgEyeClosed; } else { passwordInput.type = 'password'; button.ariaLabel = opts.ariaLabelShowPassword; - button.innerHTML = openEyeSVG; + button.innerHTML = svgEyeOpen; } passwordInput.focus(); @@ -87,44 +99,67 @@ const createShowPasswordToggleItem = (button: HTMLButtonElement) => { return span; }; -const createFormLayoutItem = (label: HTMLLabelElement, input: HTMLInputElement) => { +const createFormLayoutItem = (label: HTMLLabelElement, input: HTMLInputElement, localizationKey: string) => { const formLayoutItem = document.createElement('uui-form-layout-item') as UUIFormLayoutItemElement; + const errorId = input.getAttribute('aria-errormessage') || input.id + '-error'; formLayoutItem.appendChild(label); formLayoutItem.appendChild(input); + const validationMessage = createValidationMessage(errorId); + formLayoutItem.appendChild(validationMessage); + + // Bind validation + input.oninput = () => validateInput(input, validationMessage, localizationKey); + input.onblur = () => validateInput(input, validationMessage, localizationKey); + return formLayoutItem; }; const createFormLayoutPasswordItem = ( label: HTMLLabelElement, input: HTMLInputElement, - showPasswordToggle: HTMLSpanElement + showPasswordToggle: HTMLSpanElement, + requiredMessageKey: string ) => { const formLayoutItem = document.createElement('uui-form-layout-item') as UUIFormLayoutItemElement; + const errorId = input.getAttribute('aria-errormessage') || input.id + '-error'; formLayoutItem.appendChild(label); + const span = document.createElement('span'); span.id = 'password-input-span'; span.appendChild(input); span.appendChild(showPasswordToggle); formLayoutItem.appendChild(span); - return formLayoutItem; -}; + const validationMessage = createValidationMessage(errorId); + formLayoutItem.appendChild(validationMessage); -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'; - form.spellcheck = false; + // Bind validation + input.oninput = () => validateInput(input, validationMessage, requiredMessageKey); + input.onblur = () => validateInput(input, validationMessage, requiredMessageKey); - elements.push(styles); - elements.forEach((element) => form.appendChild(element)); + return formLayoutItem; +}; - return form; +const validateInput = (input: HTMLInputElement, validationElement: HTMLElement, requiredMessage = '') => { + validationElement.innerHTML = ''; + if (input.validity.valid) { + input.removeAttribute('aria-invalid'); + validationElement.classList.remove('active'); + validationElement.ariaLive = 'off'; + } else { + input.setAttribute('aria-invalid', 'true'); + + const localizeElement = document.createElement('umb-localize'); + localizeElement.innerHTML = input.validationMessage; + localizeElement.key = requiredMessage; + validationElement.appendChild(localizeElement); + + validationElement.classList.add('active'); + validationElement.ariaLive = 'assertive'; + } }; @customElement('umb-auth') @@ -168,16 +203,6 @@ export default class UmbAuthElement extends UmbLitElement { */ protected flow?: 'mfa' | 'reset-password' | 'invite-user'; - _form?: HTMLFormElement; - _usernameLayoutItem?: UUIFormLayoutItemElement; - _passwordLayoutItem?: UUIFormLayoutItemElement; - _usernameInput?: HTMLInputElement; - _passwordInput?: HTMLInputElement; - _usernameLabel?: HTMLLabelElement; - _passwordLabel?: HTMLLabelElement; - _passwordShowPasswordToggleItem?: HTMLSpanElement; - _passwordShowPasswordToggleButton?: HTMLButtonElement; - #authContext = new UmbAuthContext(this, UMB_AUTH_CONTEXT); constructor() { @@ -186,6 +211,16 @@ export default class UmbAuthElement extends UmbLitElement { (this as unknown as EventTarget).addEventListener('umb-login-flow', (e) => { if (e instanceof CustomEvent) { this.flow = e.detail.flow || undefined; + if (typeof e.detail.status !== 'undefined') { + const searchParams = new URLSearchParams(window.location.search); + if (e.detail.status === null) { + searchParams.delete('status'); + } else { + searchParams.set('status', e.detail.status); + } + const newRelativePathQuery = window.location.pathname + '?' + searchParams.toString(); + window.history.pushState(null, '', newRelativePathQuery); + } } this.requestUpdate(); }); @@ -229,18 +264,6 @@ export default class UmbAuthElement extends UmbLitElement { }); } - disconnectedCallback() { - super.disconnectedCallback(); - this._usernameLayoutItem?.remove(); - this._passwordLayoutItem?.remove(); - this._usernameLabel?.remove(); - this._usernameInput?.remove(); - this._passwordLabel?.remove(); - this._passwordInput?.remove(); - this._passwordShowPasswordToggleItem?.remove(); - this._passwordShowPasswordToggleButton?.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. @@ -249,48 +272,65 @@ export default class UmbAuthElement extends UmbLitElement { * @private */ #initializeForm() { - this._usernameInput = createInput({ + const usernameInput = createInput({ id: 'username-input', type: 'text', name: 'username', autocomplete: 'username', + errorId: 'username-input-error', inputmode: this.usernameIsEmail ? 'email' : '', autofocus: true, }); - this._passwordInput = createInput({ + const passwordInput = createInput({ id: 'password-input', type: 'password', name: 'password', autocomplete: 'current-password', + errorId: 'password-input-error', inputmode: '', }); - this._passwordShowPasswordToggleButton = createShowPasswordToggleButton({ + const passwordShowPasswordToggleButton = createShowPasswordToggleButton({ id: 'password-show-toggle', name: 'password-show-toggle', ariaLabelShowPassword: this.localize.term('auth_showPassword'), ariaLabelHidePassword: this.localize.term('auth_hidePassword'), }); - this._passwordShowPasswordToggleItem = createShowPasswordToggleItem(this._passwordShowPasswordToggleButton); - this._usernameLabel = createLabel({ + const passwordShowPasswordToggleItem = createShowPasswordToggleItem(passwordShowPasswordToggleButton); + const usernameLabel = createLabel({ forId: 'username-input', localizeAlias: this.usernameIsEmail ? 'auth_email' : 'auth_username', localizeFallback: this.usernameIsEmail ? 'Email' : 'Username', }); - this._passwordLabel = createLabel({ + const passwordLabel = createLabel({ forId: 'password-input', localizeAlias: 'auth_password', localizeFallback: 'Password', }); - this._usernameLayoutItem = createFormLayoutItem(this._usernameLabel, this._usernameInput); - this._passwordLayoutItem = createFormLayoutPasswordItem( - this._passwordLabel, - this._passwordInput, - this._passwordShowPasswordToggleItem + const usernameLayoutItem = createFormLayoutItem( + usernameLabel, + usernameInput, + this.usernameIsEmail ? 'auth_requiredEmailValidationMessage' : 'auth_requiredUsernameValidationMessage' ); + const passwordLayoutItem = createFormLayoutPasswordItem( + passwordLabel, + passwordInput, + passwordShowPasswordToggleItem, + 'auth_requiredPasswordValidationMessage' + ); + const style = document.createElement('style'); + style.innerHTML = authStyles; + document.head.appendChild(style); + + const form = document.createElement('form'); + form.id = 'umb-login-form'; + form.name = 'login-form'; + form.spellcheck = false; + form.setAttribute('novalidate', ''); - this._form = createForm([this._usernameLayoutItem, this._passwordLayoutItem]); + form.appendChild(usernameLayoutItem); + form.appendChild(passwordLayoutItem); - this.insertAdjacentElement('beforeend', this._form); + this.insertAdjacentElement('beforeend', form); } render() { @@ -347,12 +387,11 @@ export default class UmbAuthElement extends UmbLitElement { return html` `; default: - return html` - - - `; + return html` + + + + `; } } } diff --git a/src/Umbraco.Web.UI.Login/src/components/back-to-login-button.element.ts b/src/Umbraco.Web.UI.Login/src/components/back-to-login-button.element.ts index 9200baa52d4e..bef64afd1837 100644 --- a/src/Umbraco.Web.UI.Login/src/components/back-to-login-button.element.ts +++ b/src/Umbraco.Web.UI.Login/src/components/back-to-login-button.element.ts @@ -17,7 +17,7 @@ export default class UmbBackToLoginButtonElement extends UmbLitElement { } #handleClick() { - this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'login' } })); + this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'login', status: null } })); } static styles: CSSResultGroup = [ @@ -39,7 +39,7 @@ export default class UmbBackToLoginButtonElement extends UmbLitElement { display: inline-flex; line-height: 1; font-size: 14px; - font-family: var(--uui-font-family),sans-serif; + font-family: var(--uui-font-family), sans-serif; } button svg { width: 1rem; 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 315bac444895..d89f434a8932 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,254 +1,263 @@ import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { css, type CSSResultGroup, html, nothing, when, customElement, property, queryAssignedElements, state } from '@umbraco-cms/backoffice/external/lit'; - -import { UMB_AUTH_CONTEXT } from '../../contexts'; +import { + css, + html, + nothing, + when, + customElement, + property, + queryAssignedElements, + state, +} from '@umbraco-cms/backoffice/external/lit'; + +import { UMB_AUTH_CONTEXT } from '../../contexts/index.js'; @customElement('umb-login-page') export default class UmbLoginPageElement extends UmbLitElement { - @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; + + @state() + private _loginState?: UUIButtonState; + + @state() + private _loginError = ''; - @queryAssignedElements({flatten: true}) - protected slottedElements?: HTMLFormElement[]; + @state() + supportPersistLogin = false; - @property({type: Boolean, attribute: 'allow-password-reset'}) - allowPasswordReset = false; + #formElement?: HTMLFormElement; - @state() - private _loginState?: UUIButtonState; - - @state() - private _loginError = ''; + #authContext?: typeof UMB_AUTH_CONTEXT.TYPE; - @state() - supportPersistLogin = false; + constructor() { + super(); - #formElement?: HTMLFormElement; - - #authContext?: typeof UMB_AUTH_CONTEXT.TYPE; - - constructor() { - super(); - - this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => { - this.#authContext = authContext; - this.supportPersistLogin = authContext?.supportsPersistLogin ?? false; - }); - } - - async #onSlotChanged() { - this.#formElement = this.slottedElements?.find((el) => el.id === 'umb-login-form'); - - if (!this.#formElement) return; - - // We need to listen for the enter key to submit the form, because the uui-button does not support the native input fields submit event - this.#formElement.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - this.#onSubmitClick(); - } - }); - this.#formElement.onsubmit = this.#handleSubmit; - } - - #handleSubmit = async (e: SubmitEvent) => { - e.preventDefault(); - - if (!this.#authContext) return; - - const form = e.target as HTMLFormElement; - if (!form) return; - - const formData = new FormData(form); - - const username = formData.get('username') as string; - const password = formData.get('password') as string; - const persist = formData.has('persist'); - - if (!username || !password) { - this._loginError = this.localize.term('auth_userFailedLogin'); - this._loginState = 'failed'; - return; - } - - this._loginState = 'waiting'; - - const response = await this.#authContext.login({ - username, - password, - persist, - }); - - this._loginError = response.error || ''; - this._loginState = response.error ? 'failed' : 'success'; - - // Check for 402 status code indicating that MFA is required - if (response.status === 402) { - this.#authContext.isMfaEnabled = true; - if (response.twoFactorView) { - this.#authContext.twoFactorView = response.twoFactorView; - } - if (response.twoFactorProviders) { - this.#authContext.mfaProviders = response.twoFactorProviders; - } - - this.dispatchEvent(new CustomEvent('umb-login-flow', {composed: true, detail: {flow: 'mfa'}})); - return; - } - - if (response.error) { - return; - } - - const returnPath = this.#authContext.returnPath; - - if (returnPath) { - location.href = returnPath; - } - }; - - get #greetingLocalizationKey() { - return [ - 'auth_greeting0', - 'auth_greeting1', - 'auth_greeting2', - 'auth_greeting3', - 'auth_greeting4', - 'auth_greeting5', - 'auth_greeting6', - ][new Date().getDay()]; - } - - #onSubmitClick = () => { - this.#formElement?.requestSubmit(); - }; - - render() { - return html` - - -
    - ${when( - this.supportPersistLogin, - () => html` - - - Remember me - - ` - )} - ${when( - this.allowPasswordReset, - () => - html` - ` - )} -
    - - - ${this.#renderErrorMessage()} - `; - } - - #renderErrorMessage() { - if (!this._loginError || this._loginState !== 'failed') return nothing; - - return html`${this._loginError}`; - } - - #handleForgottenPassword() { - this.dispatchEvent(new CustomEvent('umb-login-flow', {composed: true, detail: {flow: 'reset'}})); - } - - static styles: CSSResultGroup = [ - css` - :host { - display: flex; - flex-direction: column; - } - - #header { - text-align: center; - display: flex; - flex-direction: column; - gap: var(--uui-size-space-5); - } - - #header span { - color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */ - font-size: 14px; - } - - #greeting { - color: var(--uui-color-interactive); - text-align: center; - font-weight: 400; - font-size: var(--header-font-size); - margin: 0 0 var(--uui-size-layout-1); - line-height: 1.2; - } - - #umb-login-button { - margin-top: var(--uui-size-space-4); - width: 100%; - } - - #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),sans-serif; - margin-left: auto; - margin-bottom: var(--uui-size-space-3); - } - - #forgot-password:hover { - color: var(--uui-color-interactive-emphasis); - } - - .text-error { - margin-top: var(--uui-size-space-4); - } - - .text-danger { - color: var(--uui-color-danger-standalone); - } - - #secondary-actions { - display: flex; - align-items: center; - justify-content: space-between; - } - `, - ]; + this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => { + this.#authContext = authContext; + this.supportPersistLogin = authContext?.supportsPersistLogin ?? false; + }); + } + + async #onSlotChanged() { + this.#formElement = this.slottedElements?.find((el) => el.id === 'umb-login-form'); + + if (!this.#formElement) return; + + this.#formElement.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.#onSubmitClick(); + } + }); + + this.#formElement.onsubmit = this.#handleSubmit; + } + + #handleSubmit = async (e: SubmitEvent) => { + e.preventDefault(); + + this._loginError = ''; + this._loginState = undefined; + if (!this.#authContext) return; + + const form = e.target as HTMLFormElement; + if (!form) return; + + if (!form?.checkValidity()) { + return; + } + + const formData = new FormData(form); + + const username = formData.get('username') as string; + const password = formData.get('password') as string; + const persist = formData.has('persist'); + + if (!username || !password) { + return; + } + + this._loginState = 'waiting'; + + const response = await this.#authContext.login({ + username, + password, + persist, + }); + + this._loginError = response.error || ''; + this._loginState = response.error ? 'failed' : 'success'; + + // Check for 402 status code indicating that MFA is required + if (response.status === 402) { + this.#authContext.isMfaEnabled = true; + if (response.twoFactorView) { + this.#authContext.twoFactorView = response.twoFactorView; + } + if (response.twoFactorProviders) { + this.#authContext.mfaProviders = response.twoFactorProviders; + } + + this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'mfa' } })); + return; + } + + if (response.error) { + return; + } + + const returnPath = this.#authContext.returnPath; + + if (returnPath) { + location.href = returnPath; + } + }; + + get #greetingLocalizationKey() { + return [ + 'auth_greeting0', + 'auth_greeting1', + 'auth_greeting2', + 'auth_greeting3', + 'auth_greeting4', + 'auth_greeting5', + 'auth_greeting6', + ][new Date().getDay()]; + } + + #onSubmitClick = () => { + this.#formElement?.requestSubmit(); + }; + + render() { + return html` + + +
    + ${when( + this.supportPersistLogin, + () => html` + + Remember me + + ` + )} + ${when( + this.allowPasswordReset, + () => + html` ` + )} +
    + + + ${this.#renderErrorMessage()} + `; + } + + #renderErrorMessage() { + if (!this._loginError || this._loginState !== 'failed') return nothing; + + return html`${this._loginError}`; + } + + #handleForgottenPassword() { + this.dispatchEvent(new CustomEvent('umb-login-flow', { composed: true, detail: { flow: 'reset' } })); + } + + static readonly styles = [ + css` + :host { + display: flex; + flex-direction: column; + } + + #header { + text-align: center; + display: flex; + flex-direction: column; + gap: var(--uui-size-space-5); + } + + #header span { + color: var(--uui-color-text-alt); /* TODO Change to uui color when uui gets a muted text variable */ + font-size: 14px; + } + + #greeting { + color: var(--uui-color-interactive); + text-align: center; + font-weight: 400; + font-size: var(--header-font-size); + margin: 0 0 var(--uui-size-layout-1); + line-height: 1.2; + } + + #umb-login-button { + margin-top: var(--uui-size-space-4); + width: 100%; + } + + #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), sans-serif; + margin-left: auto; + margin-bottom: var(--uui-size-space-3); + } + + #forgot-password:hover { + color: var(--uui-color-interactive-emphasis); + } + + .text-error { + margin-top: var(--uui-size-space-4); + } + + .text-danger { + color: var(--uui-color-danger-standalone); + } + + #secondary-actions { + display: flex; + align-items: center; + 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/localization/lang/da.ts b/src/Umbraco.Web.UI.Login/src/localization/lang/da.ts index dda9767dd638..4bfb57f0c717 100644 --- a/src/Umbraco.Web.UI.Login/src/localization/lang/da.ts +++ b/src/Umbraco.Web.UI.Login/src/localization/lang/da.ts @@ -1,53 +1,61 @@ import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localization-api'; export default { - auth: { - continue: 'Fortsæt', - validate: 'Indsend', - login: 'Log ind', - email: 'E-mail', - username: 'Brugernavn', - password: 'Adgangskode', - submit: 'Indsend', - required: 'Påkrævet', - success: 'Succes', - forgottenPassword: 'Glemt adgangskode?', - forgottenPasswordInstruction: 'En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode', - requestPasswordResetConfirmation: 'En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser', - setPasswordConfirmation: 'Din adgangskode er blevet opdateret', - rememberMe: 'Husk mig', - error: 'Fejl', - defaultError: 'Der er opstået en ukendt fejl.', - errorInPasswordFormat: 'Kodeordet skal være på minimum %0% tegn og indeholde mindst %1% alfanumeriske tegn.', - passwordMismatch: 'Adgangskoderne er ikke ens.', - passwordMinLength: 'Adgangskoden skal være mindst {0} tegn lang.', - passwordIsBlank: 'Din nye adgangskode kan ikke være tom.', - userFailedLogin: 'Ups! Vi kunne ikke logge dig ind. Tjek at dit brugernavn og adgangskode er korrekt og prøv igen.', - userLockedOut: 'Din konto er blevet låst. Prøv igen senere.', - receivedErrorFromServer: 'Der skete en fejl på serveren', - resetCodeExpired: 'Det link, du har klikket på, er ugyldigt eller udløbet', - userInviteWelcomeMessage: 'Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode.', - userInviteExpiredMessage: 'Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen.', - newPassword: 'Ny adgangskode', - confirmNewPassword: 'Bekræft adgangskode', - greeting0: 'Velkommen', - greeting1: 'Velkommen', - greeting2: 'Velkommen', - greeting3: 'Velkommen', - greeting4: 'Velkommen', - greeting5: 'Velkommen', - greeting6: 'Velkommen', - mfaTitle: 'Sidste skridt!', - mfaCodeInputHelp: 'Indtast venligst bekræftelseskoden', - mfaText: 'Du har aktiveret multi-faktor godkendelse. Du skal nu bekræfte din identitet.', - mfaMultipleText: 'Vælg venligst en godkendelsesmetode', - mfaCodeInput: 'Kode', - mfaInvalidCode: 'Forkert kode indtastet', - signInWith: 'Log ind med {0}', - returnToLogin: 'Tilbage til log ind', - localLoginDisabled: 'Desværre er det ikke muligt at logge ind direkte. Det er blevet deaktiveret af en login-udbyder.', - friendlyGreeting: 'Hej!', - showPassword: 'Vis adgangskode', - hidePassword: 'Skjul adgangskode', - }, + auth: { + continue: 'Fortsæt', + validate: 'Indsend', + login: 'Log ind', + email: 'E-mail', + username: 'Brugernavn', + password: 'Adgangskode', + submit: 'Indsend', + required: 'Påkrævet', + success: 'Succes', + forgottenPassword: 'Glemt adgangskode?', + forgottenPasswordInstruction: + 'En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode', + requestPasswordResetConfirmation: + 'En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser', + setPasswordConfirmation: 'Din adgangskode er blevet opdateret', + rememberMe: 'Husk mig', + error: 'Fejl', + defaultError: 'Der er opstået en ukendt fejl.', + errorInPasswordFormat: 'Kodeordet skal være på minimum %0% tegn og indeholde mindst %1% alfanumeriske tegn.', + passwordMismatch: 'Adgangskoderne er ikke ens.', + passwordMinLength: 'Adgangskoden skal være mindst {0} tegn lang.', + passwordIsBlank: 'Din nye adgangskode kan ikke være tom.', + userFailedLogin: 'Ups! Vi kunne ikke logge dig ind. Tjek at dit brugernavn og adgangskode er korrekt og prøv igen.', + userLockedOut: 'Din konto er blevet låst. Prøv igen senere.', + receivedErrorFromServer: 'Der skete en fejl på serveren', + resetCodeExpired: 'Det link, du har klikket på, er ugyldigt eller udløbet', + userInviteWelcomeMessage: + 'Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode.', + userInviteExpiredMessage: + 'Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen.', + newPassword: 'Ny adgangskode', + confirmNewPassword: 'Bekræft adgangskode', + greeting0: 'Velkommen', + greeting1: 'Velkommen', + greeting2: 'Velkommen', + greeting3: 'Velkommen', + greeting4: 'Velkommen', + greeting5: 'Velkommen', + greeting6: 'Velkommen', + mfaTitle: 'Sidste skridt!', + mfaCodeInputHelp: 'Indtast venligst bekræftelseskoden', + mfaText: 'Du har aktiveret multi-faktor godkendelse. Du skal nu bekræfte din identitet.', + mfaMultipleText: 'Vælg venligst en godkendelsesmetode', + mfaCodeInput: 'Kode', + mfaInvalidCode: 'Forkert kode indtastet', + signInWith: 'Log ind med {0}', + returnToLogin: 'Tilbage til log ind', + localLoginDisabled: + 'Desværre er det ikke muligt at logge ind direkte. Det er blevet deaktiveret af en login-udbyder.', + friendlyGreeting: 'Hej!', + requiredEmailValidationMessage: 'Udfyld venligst en e-mail', + requiredUsernameValidationMessage: 'Udfyld venligst et brugernavn', + requiredPasswordValidationMessage: 'Udfyld venligst en adgangskode', + showPassword: 'Vis adgangskode', + hidePassword: 'Skjul adgangskode', + }, } satisfies UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Login/src/localization/lang/en-us.ts b/src/Umbraco.Web.UI.Login/src/localization/lang/en-us.ts index e5fab358c3c5..40895f2513f3 100644 --- a/src/Umbraco.Web.UI.Login/src/localization/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Login/src/localization/lang/en-us.ts @@ -2,54 +2,6 @@ import type { UmbLocalizationDictionary } from '@umbraco-cms/backoffice/localiza export default { auth: { - continue: 'Continue', - validate: 'Validate', - login: 'Login', - email: 'E-mail', - username: 'Username', - password: 'Password', - submit: 'Submit', - required: 'Required', - success: 'Success', - forgottenPassword: 'Forgotten password?', - forgottenPasswordInstruction: 'An email will be sent to the address specified with a link to reset your password', - requestPasswordResetConfirmation: - 'An email with password reset instructions will be sent to the specified address if it matched our records', - setPasswordConfirmation: 'Your password has been updated', - rememberMe: 'Remember me', - error: 'Error', - defaultError: 'An error occurred while processing your request.', - errorInPasswordFormat: - 'The password must be at least {0} characters long and contain at least {1} special characters.', - passwordMismatch: 'The confirmed password does not match the new password!', - passwordMinLength: 'The password must be at least {0} characters long.', - passwordIsBlank: 'The password cannot be blank.', - userFailedLogin: "Oops! We couldn't log you in. Please check your credentials and try again.", - userLockedOut: 'Your account has been locked out. Please try again later.', - receivedErrorFromServer: 'Received an error from the server', - resetCodeExpired: 'The link you have clicked on is invalid or has expired', - userInviteWelcomeMessage: - 'Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password.', - userInviteExpiredMessage: - 'Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it.', - newPassword: 'New password', - confirmNewPassword: 'Confirm password', - greeting0: 'Welcome', - greeting1: 'Welcome', - greeting2: 'Welcome', - greeting3: 'Welcome', - greeting4: 'Welcome', - greeting5: 'Welcome', - greeting6: 'Welcome', - mfaTitle: 'One last step', - mfaCodeInputHelp: 'Enter the code from your authenticator app', - mfaText: 'You have enabled 2-factor authentication and must verify your identity.', - mfaMultipleText: 'Please choose a 2-factor provider', - mfaCodeInput: 'Verification code', - mfaInvalidCode: 'Invalid code entered', - signInWith: 'Sign in with {0}', - returnToLogin: 'Return to login', - localLoginDisabled: 'Unfortunately, direct login is not possible. It has been disabled by a provider.', friendlyGreeting: 'Hi there', }, } satisfies UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Login/src/localization/lang/en.ts b/src/Umbraco.Web.UI.Login/src/localization/lang/en.ts index 2d33fcabe302..f487ff739c49 100644 --- a/src/Umbraco.Web.UI.Login/src/localization/lang/en.ts +++ b/src/Umbraco.Web.UI.Login/src/localization/lang/en.ts @@ -29,7 +29,7 @@ export default { receivedErrorFromServer: 'Received an error from the server', resetCodeExpired: 'The link you have clicked on is invalid or has expired', userInviteWelcomeMessage: - 'Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password.', + "Hello there and welcome to Umbraco! In just 1 minute you'll be good to go, we just need you to setup a password.", userInviteExpiredMessage: 'Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it.', newPassword: 'New password', @@ -51,6 +51,9 @@ export default { returnToLogin: 'Return to login', localLoginDisabled: 'Unfortunately, direct login is not possible. It has been disabled by a provider.', friendlyGreeting: 'Hello', + requiredEmailValidationMessage: 'Please fill in an email', + requiredUsernameValidationMessage: 'Please fill in a username', + requiredPasswordValidationMessage: 'Please fill in a password', showPassword: 'Show password', hidePassword: 'Hide password', }, From 89989d60ce9fd1f4f49714ea823d025a61695254 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:00:43 +0100 Subject: [PATCH 128/155] Templates: Fix "Discard changes?" dialog after creating template with master template (fixes #20262) (#20749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the _data.updateCurrent() call inside the updateLayoutBlock conditional in setMasterTemplate(). This prevents spurious change detection when loading templates from the server, while maintaining proper change tracking when users actually modify the master template via the UI. This completes the fix started in PR #20529 which added the updateLayoutBlock parameter but inadvertently left the data model update outside the conditional. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .../templates/workspace/template-workspace.context.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts index 0ea8f97d8cdd..0951d51f308b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts @@ -123,10 +123,9 @@ export class UmbTemplateWorkspaceContext if (updateLayoutBlock) { this.#updateMasterTemplateLayoutBlock(); + this._data.updateCurrent({ masterTemplate: unique ? { unique } : null }); } - this._data.updateCurrent({ masterTemplate: unique ? { unique } : null }); - return unique; } From ab51aac5c64250ab785dde233af322d95f8669e4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:57:24 +0100 Subject: [PATCH 129/155] Backoffice Item Pickers: Show error for missing items in 10 picker types (closes #19329, #20270, #20367) (#20762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add errorDetail property to umb-entity-item-ref Add optional errorDetail property to display additional context (such as file paths or IDs) in error states. This enhances the error display to show both the error message and relevant details. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Make _removeItem protected in UmbPickerInputContext Change #removeItem from private to protected to allow subclasses to reuse the removal logic while customizing the confirmation dialog. This enables better extensibility for specialized picker contexts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix static file picker to show error state for missing files Update umb-input-static-file to observe statuses and render based on item state (loading, error, success). When a static file is missing (API returns empty array), displays error state with alert icon and file path detail using umb-entity-item-ref. Also adds standalone property support for proper single-item styling. Fixes #19329 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Show file path in static file remove confirmation dialog Override requestRemoveItem in UmbStaticFilePickerInputContext to display the file path instead of "Not found" in the confirmation dialog when removing missing static files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Show GUID in document picker error state Display the document GUID as errorDetail when a document is not found (deleted/gone). This provides useful context for editors to identify which document was referenced. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Show GUID in document picker remove confirmation dialog Display the document GUID instead of "Not found" in the remove confirmation dialog when the document no longer exists. This provides useful context for editors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix: apply the temp model which the context uses * Refactor: Move requestRemoveItem logic to base UmbPickerInputContext Eliminated duplicate code across three picker contexts by: - Adding protected getItemDisplayName() method to base class - Moving requestRemoveItem implementation to base class - Removing duplicate implementations from document, member, and static file pickers - Static file picker overrides getItemDisplayName() to show file path Net reduction: 19 lines of code (69 removed, 50 added) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Document Type Picker: Show error state for missing items (fixes #20367) Apply the same error state handling to the document type picker that was implemented for static files, documents, and members. When a referenced document type is missing or deleted: - Show error state with the GUID as errorDetail - Allow removal with proper confirmation dialog - Use umb-entity-item-ref for error display - Use uui-ref-node-document-type for successful items 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Additional pickers: Show error states for missing items in user, language, media-type, member-type, member-group, and user-group pickers Apply the same error state handling pattern to six additional picker types: - user-input: Users - input-language: Languages - input-media-type: Media types - input-member-type: Member types - input-member-group: Member groups - user-group-input: User groups All pickers now: - Observe statuses from UmbRepositoryItemsManager - Show error state with GUID when referenced item is missing/deleted - Use umb-entity-item-ref for error display - Use specialized components (uui-ref-node, umb-user-group-ref, etc.) for successful items - Allow removal with proper confirmation dialog showing GUID Maintains code reusability by using the base class requestRemoveItem method with getItemDisplayName() for consistent error handling across all pickers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Lint: Remove unused 'when' imports from input-media-type and user-group-input * Refactor: Add #renderItem helper method to all pickers for consistency - Add #renderItem to user-input (extracted from inline repeat callback) - Change _renderItem to #renderItem in user-group-input for consistency - Change _renderItem to #renderItem in input-static-file for consistency All 10 pickers now use consistent #renderItem helper method pattern, improving code readability and maintainability as suggested by @nielslyngsoe * `import` sorting * Corrected (old) JSDoc typos * Markup tidy-up * exported `UmbPropertyEditorUIStaticFilePickerElement` as `element` --------- Co-authored-by: Claude Co-authored-by: leekelleher --- .../entity-item-ref.element.ts | 53 ++++++----- .../core/picker-input/picker-input.context.ts | 25 +++-- .../input-document-type.element.ts | 95 +++++++++++++------ .../input-document/input-document.element.ts | 40 ++++---- .../input-language/input-language.element.ts | 60 +++++++----- .../input-media-type.element.ts | 71 ++++++++++---- .../input-member-group.element.ts | 95 +++++++++++++------ .../input-member-type.element.ts | 48 ++++++++-- .../input-member/input-member.element.ts | 60 +++++++----- .../input-static-file.context.ts | 10 +- .../input-static-file.element.ts | 37 +++++--- .../static-file-picker/manifests.ts | 2 +- ...ty-editor-ui-static-file-picker.element.ts | 2 + .../user-group-input.element.ts | 60 ++++++++++-- .../user-input/user-input.element.ts | 35 +++++-- 15 files changed, 479 insertions(+), 214 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts index 97b469d8a5dc..836007d29cd2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts @@ -1,15 +1,16 @@ import type { ManifestEntityItemRef } from './entity-item-ref.extension.js'; -import { customElement, property, type PropertyValueMap, state, css, html } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; -import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; import { UUIBlinkAnimationValue } from '@umbraco-cms/backoffice/external/uui'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import './default-item-ref.element.js'; -import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; @customElement('umb-entity-item-ref') export class UmbEntityItemRefElement extends UmbLitElement { @@ -20,9 +21,6 @@ export class UmbEntityItemRefElement extends UmbLitElement { private _component?: any; // TODO: Add type @property({ type: Object, attribute: false }) - public get item(): UmbEntityModel | undefined { - return this.#item; - } public set item(value: UmbEntityModel | undefined) { const oldValue = this.#item; this.#item = value; @@ -41,6 +39,9 @@ export class UmbEntityItemRefElement extends UmbLitElement { // If the component is already created, but the entity type is different, we need to destroy the component. this.#createController(value.entityType); } + public get item(): UmbEntityModel | undefined { + return this.#item; + } #readonly = false; @property({ type: Boolean, reflect: true }) @@ -124,20 +125,23 @@ export class UmbEntityItemRefElement extends UmbLitElement { error?: boolean; @property({ type: String, attribute: 'error-message', reflect: false }) - errorMessage?: string; + errorMessage?: string | null; + + @property({ type: String, attribute: 'error-detail', reflect: false }) + errorDetail?: string | null; #pathAddendum = new UmbRoutePathAddendumContext(this); #onSelected(event: UmbSelectedEvent) { event.stopPropagation(); - const unique = this.#item?.unique; + const unique = this.item?.unique; if (!unique) throw new Error('No unique id found for item'); this.dispatchEvent(new UmbSelectedEvent(unique)); } #onDeselected(event: UmbDeselectedEvent) { event.stopPropagation(); - const unique = this.#item?.unique; + const unique = this.item?.unique; if (!unique) throw new Error('No unique id found for item'); this.dispatchEvent(new UmbDeselectedEvent(unique)); } @@ -163,7 +167,7 @@ export class UmbEntityItemRefElement extends UmbLitElement { // TODO: I would say this code can use feature of the UmbExtensionsElementInitializer, to set properties and get a fallback element. [NL] // assign the properties to the component - component.item = this.#item; + component.item = this.item; component.readonly = this.readonly; component.standalone = this.standalone; component.selectOnly = this.selectOnly; @@ -192,20 +196,25 @@ export class UmbEntityItemRefElement extends UmbLitElement { if (this._component) { return html`${this._component}`; } + // Error: if (this.error) { - return html` - - - `; + return html` + + + + + `; } + // Loading: return html``; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index 2829e753db6b..9c90d216c05f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -31,8 +31,8 @@ export class UmbPickerInputContext< public readonly interactionMemory = new UmbInteractionMemoryManager(this); /** - * Define a minimum amount of selected items in this input, for this input to be valid. - * @returns {number} The minimum number of items required. + * Define a maximum amount of selected items in this input, for this input to be valid. + * @returns {number} The maximum number of items required. */ public get max() { return this._max; @@ -43,7 +43,7 @@ export class UmbPickerInputContext< private _max = Infinity; /** - * Define a maximum amount of selected items in this input, for this input to be valid. + * Define a minimum amount of selected items in this input, for this input to be valid. * @returns {number} The minimum number of items required. */ public get min() { @@ -111,21 +111,32 @@ export class UmbPickerInputContext< this.getHostElement().dispatchEvent(new UmbChangeEvent()); } + /** + * Get the display name for an item to show in the remove confirmation dialog. + * Subclasses can override this to provide custom formatting for missing items. + * @param item - The item to get the display name for, or undefined if not found + * @param unique - The unique identifier of the item + * @returns The display name to show in the dialog + */ + protected getItemDisplayName(item: PickedItemType | undefined, unique: string): string { + return item?.name ?? unique; + } + async requestRemoveItem(unique: string) { const item = this.#itemManager.getItems().find((item) => item.unique === unique); + const name = this.getItemDisplayName(item, unique); - const name = item?.name ?? '#general_notFound'; await umbConfirmModal(this, { color: 'danger', - headline: `#actions_remove ${name}?`, + headline: `#actions_remove?`, content: `#defaultdialogs_confirmremove ${name}?`, confirmLabel: '#actions_remove', }); - this.#removeItem(unique); + this._removeItem(unique); } - #removeItem(unique: string) { + protected _removeItem(unique: string) { const newSelection = this.getSelection().filter((value) => value !== unique); this.setSelection(newSelection); this.getHostElement().dispatchEvent(new UmbChangeEvent()); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts index 01f74f88bce0..cbff030a4c85 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts @@ -2,13 +2,16 @@ import type { UmbDocumentTypeItemModel, UmbDocumentTypeTreeItemModel } from '../ import { UMB_DOCUMENT_TYPE_WORKSPACE_MODAL } from '../../constants.js'; import { UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN } from '../../paths.js'; import { UmbDocumentTypePickerInputContext } from './input-document-type.context.js'; -import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-document-type') export class UmbInputDocumentTypeElement extends UmbFormControlMixin( @@ -112,6 +115,9 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + @state() private _editPath = ''; @@ -143,6 +149,7 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -151,8 +158,8 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin !x.isFolder && x.isElement === false; } @@ -184,8 +191,8 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + ${when( + !this.readonly, + () => html` + + this.#removeItem(unique)}> + + `, + )} + + `; + } + + // For successful items, use the document type specific component + if (!item) return nothing; + const href = this._editPath + UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique }); + return html` + + ${this.#renderIcon(item)} + + ${when( + !this.readonly, + () => html` + this.#removeItem(unique)}> + `, + )} + + + `; + }, )} `; } - #renderItem(item: UmbDocumentTypeItemModel) { - if (!item.unique) return; - const href = this._editPath + UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique: item.unique }); - return html` - - ${this.#renderIcon(item)} - - ${when( - !this.readonly, - () => html` - this.#removeItem(item)}> - `, - )} - - - `; - } - #renderIcon(item: UmbDocumentTypeItemModel) { if (!item.icon) return; return html``; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index 516251a4b5b9..302d9594c7ee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -239,24 +239,28 @@ export class UmbInputDocumentElement extends UmbFormControlMixin { const unique = status.unique; const item = this._items?.find((x) => x.unique === unique); - return html` - ${when( - !this.readonly, - () => html` - - this.#onRemove(unique)}> - - `, - )} - `; + const isError = status.state.type === 'error'; + return html` + + ${when( + !this.readonly, + () => html` + + this.#onRemove(unique)}> + + `, + )} + + `; }, )} diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts index 826bbe0b83d6..9a462669437c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts @@ -6,6 +6,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; @customElement('umb-input-language') export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -17,7 +18,7 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, return modelEntry; }, identifier: 'Umb.SorterIdentifier.InputLanguage', - itemSelector: 'uui-ref-node', + itemSelector: 'umb-entity-item-ref', containerSelector: 'uui-ref-list', onChange: ({ model }) => { this.selection = model; @@ -115,6 +116,9 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, @state() private _items: Array = []; + @state() + private _statuses?: Array; + #pickerContext = new UmbLanguagePickerInputContext(this); constructor() { @@ -134,6 +138,7 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -147,8 +152,8 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, }); } - #onRemove(item: UmbLanguageItemModel) { - this.#pickerContext.requestRemoveItem(item.unique); + #onRemove(unique: string) { + this.#pickerContext.requestRemoveItem(unique); } override render() { @@ -167,29 +172,38 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, } #renderItems() { - if (!this._items) return; + if (!this._statuses) return; return html` ${repeat( - this._items, - (item) => item.unique, - (item) => - html` - ${when( - !this.readonly, - () => html` - - this.#onRemove(item)}> - - `, - )} - `, + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + return html` + + ${when( + !this.readonly, + () => html` + + this.#onRemove(unique)}> + + `, + )} + + `; + }, )} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts index cd659f441a16..d9e7949db0f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts @@ -8,6 +8,9 @@ import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-media-type') export class UmbInputMediaTypeElement extends UmbFormControlMixin( @@ -95,6 +98,9 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + @state() private _editPath = ''; @@ -126,6 +132,7 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -138,8 +145,8 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + + this.#removeItem(unique)}> + + + `; + } + + // For successful items, use the media type specific component + if (!item) return nothing; + const href = `${this._editPath}edit/${unique}`; + return html` + + ${this.#renderIcon(item)} + + + this.#removeItem(unique)}> + + + `; + }, )} `; } - #renderItem(item: UmbMediaTypeItemModel) { - if (!item.unique) return; - const href = `${this._editPath}edit/${item.unique}`; - return html` - - ${this.#renderIcon(item)} - - - this.#removeItem(item)} label=${this.localize.term('general_remove')}> - - - `; - } - #renderIcon(item: UmbMediaTypeItemModel) { if (!item.icon) return; return html``; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts index bec7d38b13a7..40a8c9e3d5da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts @@ -1,13 +1,16 @@ import type { UmbMemberGroupItemModel } from '../../types.js'; import { UmbMemberGroupPickerInputContext } from './input-member-group.context.js'; -import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-member-group') export class UmbInputMemberGroupElement extends UmbFormControlMixin( @@ -124,6 +127,9 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + #pickerContext = new UmbMemberGroupPickerInputContext(this); constructor() { @@ -152,6 +158,7 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observeItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -164,8 +171,8 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + ${when( + !this.readonly, + () => html` + + this.#removeItem(unique)}> + + `, + )} + + `; + } + + // For successful items, use uui-ref-node + if (!item) return nothing; + return html` + + + ${when( + !this.readonly, + () => + html` this.#removeItem(unique)} + label=${this.localize.term('general_remove')}>`, + )} + + + + `; + }, )} `; @@ -199,27 +255,6 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin - ${this.#renderRemoveButton(item)} - - - `; - } - - #renderRemoveButton(item: UmbMemberGroupItemModel) { - if (this.readonly) return nothing; - return html` this.#removeItem(item)} - label=${this.localize.term('general_remove')}>`; - } - static override styles = [ css` #btn-add { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts index dd887b2d66ee..6386a906c1de 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts @@ -1,9 +1,12 @@ import { UmbMemberTypePickerInputContext } from './input-member-type.context.js'; -import { css, html, customElement, property, state, repeat, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; import type { UmbUniqueItemModel } from '@umbraco-cms/backoffice/models'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-member-type') export class UmbInputMemberTypeElement extends UmbFormControlMixin( @@ -73,6 +76,9 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + #pickerContext = new UmbMemberTypePickerInputContext(this); constructor() { @@ -92,6 +98,7 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin (this.value = selection.join(','))); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -109,13 +116,13 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => this.#renderItem(status), )} `; @@ -134,14 +141,37 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + + this.#pickerContext.requestRemoveItem(unique)}> + + + `; + } + + // For successful items, use the member type specific component + if (!item?.unique) return nothing; return html` ${when(item.icon, () => html``)} this.#pickerContext.requestRemoveItem(item.unique!)} + @click=${() => this.#pickerContext.requestRemoveItem(unique)} label="Remove Member Type ${item.name}" >${this.localize.term('general_remove')} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts index d5b60d9e0488..8ddf6e606824 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts @@ -1,12 +1,13 @@ import type { UmbMemberItemModel } from '../../item/types.js'; import { UmbMemberPickerInputContext } from './input-member.context.js'; -import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/member-type'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; @customElement('umb-input-member') export class UmbInputMemberElement extends UmbFormControlMixin( @@ -121,6 +122,9 @@ export class UmbInputMemberElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + #pickerContext = new UmbMemberPickerInputContext(this); constructor() { @@ -140,6 +144,7 @@ export class UmbInputMemberElement extends UmbFormControlMixin (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observeItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } #openPicker() { @@ -156,8 +161,8 @@ export class UmbInputMemberElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + return html` + + ${when( + !this.readonly, + () => html` + + this.#onRemove(unique)}> + + `, + )} + + `; + }, )} `; } - #renderItem(item: UmbMemberItemModel) { - if (!item.unique) return nothing; - return html` - - ${this.#renderRemoveButton(item)} - - `; - } - #renderAddButton() { if (this.selection.length >= this.max) return nothing; if (this.readonly && this.selection.length > 0) { @@ -202,13 +223,6 @@ export class UmbInputMemberElement extends UmbFormControlMixin this.#onRemove(item)} label=${this.localize.term('general_remove')}>
    - `; - } - static override styles = [ css` #btn-add { diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.context.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.context.ts index 7a6bc3826a84..75bbb7e31cfc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.context.ts @@ -1,8 +1,9 @@ import { UMB_STATIC_FILE_PICKER_MODAL } from '../../modals/index.js'; -import type { UmbStaticFilePickerModalData, UmbStaticFilePickerModalValue } from '../../modals/index.js'; import { UMB_STATIC_FILE_ITEM_REPOSITORY_ALIAS } from '../../constants.js'; +import type { UmbStaticFilePickerModalData, UmbStaticFilePickerModalValue } from '../../modals/index.js'; import type { UmbStaticFileItemModel } from '../../types.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbStaticFilePickerInputContext extends UmbPickerInputContext< @@ -11,7 +12,14 @@ export class UmbStaticFilePickerInputContext extends UmbPickerInputContext< UmbStaticFilePickerModalData, UmbStaticFilePickerModalValue > { + #serializer = new UmbServerFilePathUniqueSerializer(); + constructor(host: UmbControllerHost) { super(host, UMB_STATIC_FILE_ITEM_REPOSITORY_ALIAS, UMB_STATIC_FILE_PICKER_MODAL); } + + protected override getItemDisplayName(item: UmbStaticFileItemModel | undefined, unique: string): string { + // If item doesn't exist, use the file path as the name + return item?.name ?? this.#serializer.toServerPath(unique) ?? unique; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts index 9e42a0b6f7b1..e533364de471 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts @@ -2,9 +2,12 @@ import type { UmbStaticFileItemModel } from '../../repository/item/types.js'; import { UmbStaticFilePickerInputContext } from './input-static-file.context.js'; import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-static-file') export class UmbInputStaticFileElement extends UmbFormControlMixin( @@ -79,6 +82,9 @@ export class UmbInputStaticFileElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + #pickerContext = new UmbStaticFilePickerInputContext(this); constructor() { @@ -98,6 +104,7 @@ export class UmbInputStaticFileElement extends UmbFormControlMixin (this.value = selection.join(','))); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses)); } protected override getFormElement() { @@ -105,13 +112,13 @@ export class UmbInputStaticFileElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this._renderItem(item), + this._statuses, + (status) => status.unique, + (status) => this.#renderItem(status), )} ${this.#renderAddButton()} @@ -137,17 +144,25 @@ export class UmbInputStaticFileElement extends UmbFormControlMixin x.unique === unique); + const isError = status.state.type === 'error'; + return html` - - + this.#pickerContext.requestRemoveItem(item.unique)}> + @click=${() => this.#pickerContext.requestRemoveItem(unique)}> - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/manifests.ts index cd9c2b9c6406..261047fb1598 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/manifests.ts @@ -4,7 +4,7 @@ export const manifest: ManifestPropertyEditorUi = { type: 'propertyEditorUi', alias: 'Umb.PropertyEditorUi.StaticFilePicker', name: 'Static File Picker Property Editor UI', - js: () => import('./property-editor-ui-static-file-picker.element.js'), + element: () => import('./property-editor-ui-static-file-picker.element.js'), meta: { label: 'Static File Picker', icon: 'icon-document', diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/property-editor-ui-static-file-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/property-editor-ui-static-file-picker.element.ts index 81cfbda18ddb..d5098b7700bf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/property-editor-ui-static-file-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/property-editor-ui-static-file-picker.element.ts @@ -73,6 +73,8 @@ export class UmbPropertyEditorUIStaticFilePickerElement extends UmbLitElement im } } +export { UmbPropertyEditorUIStaticFilePickerElement as element }; + export default UmbPropertyEditorUIStaticFilePickerElement; declare global { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts index f509aee05805..d86884dc9007 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts @@ -1,12 +1,24 @@ import { UMB_USER_GROUP_ENTITY_TYPE } from '../../entity.js'; import type { UmbUserGroupItemModel } from '../../repository/index.js'; import { UmbUserGroupPickerInputContext } from './user-group-input.context.js'; -import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import { + css, + customElement, + html, + ifDefined, + nothing, + property, + repeat, + state, +} from '@umbraco-cms/backoffice/external/lit'; +import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; -import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; +import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; +import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-user-group-input') export class UmbUserGroupInputElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -75,6 +87,9 @@ export class UmbUserGroupInputElement extends UUIFormControlMixin(UmbLitElement, @state() private _items?: Array; + @state() + private _statuses?: Array; + #pickerContext = new UmbUserGroupPickerInputContext(this); @state() @@ -97,6 +112,7 @@ export class UmbUserGroupInputElement extends UUIFormControlMixin(UmbLitElement, this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) .addAdditionalPath(UMB_USER_GROUP_ENTITY_TYPE) @@ -114,7 +130,15 @@ export class UmbUserGroupInputElement extends UUIFormControlMixin(UmbLitElement, override render() { return html` - ${this._items?.map((item) => this._renderItem(item))} + + ${this._statuses + ? repeat( + this._statuses, + (status) => status.unique, + (status) => this.#renderItem(status), + ) + : nothing} + x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + + this.#pickerContext.requestRemoveItem(unique)}> + + + `; + } + + // For successful items, use umb-user-group-ref + if (!item?.unique) return nothing; + const href = `${this._editUserGroupPath}edit/${unique}`; return html` ${item.icon ? html`` : nothing} this.#pickerContext.requestRemoveItem(item.unique)} + @click=${() => this.#pickerContext.requestRemoveItem(unique)} label=${this.localize.term('general_remove')}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts index f5621924100a..a1de877671c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts @@ -6,6 +6,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; // TODO: Shall we rename to 'umb-input-user'? [LK] @customElement('umb-user-input') @@ -92,6 +93,9 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') @state() private _items?: Array; + @state() + private _statuses?: Array; + #pickerContext = new UmbUserPickerInputContext(this); constructor() { @@ -111,6 +115,7 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -121,8 +126,8 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') this.#pickerContext.openPicker({}); } - #removeItem(item: UmbUserItemModel) { - this.#pickerContext.requestRemoveItem(item.unique); + #removeItem(unique: string) { + this.#pickerContext.requestRemoveItem(unique); } override render() { @@ -141,24 +146,34 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') } #renderItems() { - if (!this._items) return nothing; + if (!this._statuses) return nothing; return html` ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => this.#renderItem(status), )} `; } - #renderItem(item: UmbUserItemModel) { - if (!item.unique) return nothing; + #renderItem(status: UmbRepositoryItemsStatus) { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; return html` - + - this.#removeItem(item)}> + this.#removeItem(unique)}> `; From 9fa382e84dd8c6f257db24bdea5e60821fbc4daa Mon Sep 17 00:00:00 2001 From: Andrej Davidovic Date: Mon, 10 Nov 2025 18:17:30 +0200 Subject: [PATCH 130/155] Fix block list inline mode (#20745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix block list inline mode https://github.com/umbraco/Umbraco-CMS/issues/20618 * Fixed potential runtime errors * Code cleanup * Fixed Code Health Review * Revert some changes Commented out unused state properties and related code. * Remove commented-out state property in block workspace view * fix localization * no need for question mark after ids, they should be presented as required --------- Co-authored-by: Niels Lyngsø Co-authored-by: Niels Lyngsø --- ...ace-view-edit-content-no-router.element.ts | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts index c1fd3ba23194..303f0f0cd159 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts @@ -14,7 +14,7 @@ import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace' */ @customElement('umb-block-workspace-view-edit-content-no-router') export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitElement implements UmbWorkspaceViewElement { - //private _hasRootProperties = false; + // private _hasRootProperties = false; @state() private _hasRootGroups = false; @@ -25,9 +25,6 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme @state() private _activeTabKey?: string | null | undefined; - //@state() - //private _activeTabName?: string | null | undefined; - #blockWorkspace?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; #tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); @@ -67,46 +64,45 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme #checkDefaultTabName() { if (!this._tabs || !this.#blockWorkspace) return; - // Find the default tab to grab: + // Find the default tab to grab if (this._activeTabKey === undefined) { if (this._hasRootGroups) { - //this._activeTabName = null; this._activeTabKey = null; } else if (this._tabs.length > 0) { - //this._activeTabName = this._tabs[0].name; - this._activeTabKey = this._tabs[0].key; + const tab = this._tabs[0]; + this._activeTabKey = tab.ownerId ?? tab.ids[0]; } } } - #setTabName(tabName: string | undefined | null, tabKey: string | null | undefined) { - //this._activeTabName = tabName; + #setTabKey(tabKey: string | null | undefined) { this._activeTabKey = tabKey; } override render() { if (!this._tabs) return; + return html` ${this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups) - ? html` + ? html` ${this._hasRootGroups && this._tabs.length > 0 - ? html` - this.#setTabName(null, null)} - >Content - ` + ? html` this.#setTabKey(null)} + >Content` : nothing} ${repeat( this._tabs, (tab) => tab.name, (tab) => { + const tabKey = tab.ownerId ?? tab.ids[0]; + return html` this.#setTabName(tab.name, tab.key)} + label=${this.localize.string(tab.name ?? '#general_unnamed')} + .active=${this._activeTabKey === tabKey} + @click=${() => this.#setTabKey(tabKey)} >${tab.name}`; }, From 12b483ff051978616df2fb93b8c0adc5573a15bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 10 Nov 2025 17:42:16 +0100 Subject: [PATCH 131/155] Fix block list inline mode (#20745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix block list inline mode https://github.com/umbraco/Umbraco-CMS/issues/20618 * Fixed potential runtime errors * Code cleanup * Fixed Code Health Review * Revert some changes Commented out unused state properties and related code. * Remove commented-out state property in block workspace view * fix localization * no need for question mark after ids, they should be presented as required --------- Co-authored-by: Niels Lyngsø Co-authored-by: Niels Lyngsø --- ...ace-view-edit-content-no-router.element.ts | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts index c1fd3ba23194..303f0f0cd159 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-content-no-router.element.ts @@ -14,7 +14,7 @@ import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace' */ @customElement('umb-block-workspace-view-edit-content-no-router') export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitElement implements UmbWorkspaceViewElement { - //private _hasRootProperties = false; + // private _hasRootProperties = false; @state() private _hasRootGroups = false; @@ -25,9 +25,6 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme @state() private _activeTabKey?: string | null | undefined; - //@state() - //private _activeTabName?: string | null | undefined; - #blockWorkspace?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; #tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); @@ -67,46 +64,45 @@ export class UmbBlockWorkspaceViewEditContentNoRouterElement extends UmbLitEleme #checkDefaultTabName() { if (!this._tabs || !this.#blockWorkspace) return; - // Find the default tab to grab: + // Find the default tab to grab if (this._activeTabKey === undefined) { if (this._hasRootGroups) { - //this._activeTabName = null; this._activeTabKey = null; } else if (this._tabs.length > 0) { - //this._activeTabName = this._tabs[0].name; - this._activeTabKey = this._tabs[0].key; + const tab = this._tabs[0]; + this._activeTabKey = tab.ownerId ?? tab.ids[0]; } } } - #setTabName(tabName: string | undefined | null, tabKey: string | null | undefined) { - //this._activeTabName = tabName; + #setTabKey(tabKey: string | null | undefined) { this._activeTabKey = tabKey; } override render() { if (!this._tabs) return; + return html` ${this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups) - ? html` + ? html` ${this._hasRootGroups && this._tabs.length > 0 - ? html` - this.#setTabName(null, null)} - >Content - ` + ? html` this.#setTabKey(null)} + >Content` : nothing} ${repeat( this._tabs, (tab) => tab.name, (tab) => { + const tabKey = tab.ownerId ?? tab.ids[0]; + return html` this.#setTabName(tab.name, tab.key)} + label=${this.localize.string(tab.name ?? '#general_unnamed')} + .active=${this._activeTabKey === tabKey} + @click=${() => this.#setTabKey(tabKey)} >${tab.name}`; }, From d8198d2f5cf540a33ca90e1d80fc7d02fd0281bb Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Tue, 11 Nov 2025 06:33:31 +0100 Subject: [PATCH 132/155] Accessibility: Adding a label attribute for `` in news dashboard (#20780) Added 'label attribute to the uui-button in the umb-news.card.element + Removing the redundant text for uui-button since label attribute is now present --- .../umbraco-news/components/umb-news-card.element.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-card.element.ts index 133f5e53c546..9e0055f406bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/components/umb-news-card.element.ts @@ -36,9 +36,12 @@ export class UmbNewsCardElement extends UmbLitElement { ${this.item.body ? html`
    ${unsafeHTML(this.item.body)}
    ` : nothing} ${!isLastRow && this.item.url ? html`
    - - ${this.item.buttonText || 'Open'} - +
    ` : nothing}
    From cfa32b265a52510479ee6d97dbf3e8957c733c01 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 11 Nov 2025 06:59:25 +0100 Subject: [PATCH 133/155] Integration Tests: Avoid asserting on errors for permission tests (#20643) * Added integration tests for PropertyTypeUsageService and adjusted assert in management API permissions test. * Commented or fixed management API integration tests verifying permissions where we were asserting on an error response. --- .../IPropertyTypeUsageRepository.cs | 10 +++++ .../Services/IPropertyTypeUsageService.cs | 3 ++ .../Services/PropertyTypeUsageService.cs | 10 ++++- .../Implement/PropertyTypeUsageRepository.cs | 38 ++++++++----------- ...ExecuteActionHealthCheckControllerTests.cs | 4 +- .../LogViewer/AllLogViewerControllerTests.cs | 2 +- ...MessageTemplateLogViewerControllerTests.cs | 2 +- .../LogLevelCountLogViewerControllerTests.cs | 2 +- ...dateLogFileSizeLogViewerControllerTests.cs | 2 +- .../IsUsedPropertyTypeControllerTests.cs | 2 +- .../User/InviteUserControllerTests.cs | 2 +- .../UmbracoIntegrationTestWithContent.cs | 5 ++- .../Services/PropertyTypeUsageServiceTests.cs | 26 +++++++++++++ 13 files changed, 73 insertions(+), 35 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/PropertyTypeUsageServiceTests.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/IPropertyTypeUsageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPropertyTypeUsageRepository.cs index 7aa05fc1ffc0..d0e3eb356c09 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IPropertyTypeUsageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IPropertyTypeUsageRepository.cs @@ -1,7 +1,17 @@ namespace Umbraco.Cms.Core.Persistence.Repositories; +/// +/// Defines repository methods for querying property type usage. +/// public interface IPropertyTypeUsageRepository { + /// + /// Determines whether there are any saved property values for the specified content type and property alias. + /// Task HasSavedPropertyValuesAsync(Guid contentTypeKey, string propertyAlias); + + /// + /// Determines whether a content type with the specified unique identifier exists. + /// Task ContentTypeExistAsync(Guid contentTypeKey); } diff --git a/src/Umbraco.Core/Services/IPropertyTypeUsageService.cs b/src/Umbraco.Core/Services/IPropertyTypeUsageService.cs index df061ca628c4..5497e575c50a 100644 --- a/src/Umbraco.Core/Services/IPropertyTypeUsageService.cs +++ b/src/Umbraco.Core/Services/IPropertyTypeUsageService.cs @@ -2,6 +2,9 @@ namespace Umbraco.Cms.Core.Services; +/// +/// Defines service methods for querying property type usage. +/// public interface IPropertyTypeUsageService { /// diff --git a/src/Umbraco.Core/Services/PropertyTypeUsageService.cs b/src/Umbraco.Core/Services/PropertyTypeUsageService.cs index d22bdeb44048..579a338ecdaa 100644 --- a/src/Umbraco.Core/Services/PropertyTypeUsageService.cs +++ b/src/Umbraco.Core/Services/PropertyTypeUsageService.cs @@ -4,19 +4,25 @@ namespace Umbraco.Cms.Core.Services; +/// public class PropertyTypeUsageService : IPropertyTypeUsageService { private readonly IPropertyTypeUsageRepository _propertyTypeUsageRepository; - private readonly IContentTypeService _contentTypeService; private readonly ICoreScopeProvider _scopeProvider; + // TODO (V18): Remove IContentTypeService parameter from constructor. + + /// + /// Initializes a new instance of the class. + /// public PropertyTypeUsageService( IPropertyTypeUsageRepository propertyTypeUsageRepository, +#pragma warning disable IDE0060 // Remove unused parameter IContentTypeService contentTypeService, +#pragma warning restore IDE0060 // Remove unused parameter ICoreScopeProvider scopeProvider) { _propertyTypeUsageRepository = propertyTypeUsageRepository; - _contentTypeService = contentTypeService; _scopeProvider = scopeProvider; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PropertyTypeUsageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PropertyTypeUsageRepository.cs index ab9a03bcefa1..64917f9af149 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PropertyTypeUsageRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PropertyTypeUsageRepository.cs @@ -1,4 +1,3 @@ - using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; @@ -8,28 +7,26 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +/// internal sealed class PropertyTypeUsageRepository : IPropertyTypeUsageRepository { - private static readonly Guid?[] NodeObjectTypes = new Guid?[] - { + private static readonly List _nodeObjectTypes = + [ Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MemberType, - }; + ]; private readonly IScopeAccessor _scopeAccessor; - public PropertyTypeUsageRepository(IScopeAccessor scopeAccessor) - { - _scopeAccessor = scopeAccessor; - } + /// + /// Initializes a new instance of the class. + /// + public PropertyTypeUsageRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + /// public Task HasSavedPropertyValuesAsync(Guid contentTypeKey, string propertyAlias) { - IUmbracoDatabase? database = _scopeAccessor.AmbientScope?.Database; - - if (database is null) - { - throw new InvalidOperationException("A scope is required to query the database"); - } + IUmbracoDatabase? database = _scopeAccessor.AmbientScope?.Database + ?? throw new InvalidOperationException("A scope is required to query the database"); Sql selectQuery = database.SqlContext.Sql() .SelectAll() @@ -47,26 +44,21 @@ public Task HasSavedPropertyValuesAsync(Guid contentTypeKey, string proper return Task.FromResult(database.ExecuteScalar(hasValuesQuery)); } + /// public Task ContentTypeExistAsync(Guid contentTypeKey) { - IUmbracoDatabase? database = _scopeAccessor.AmbientScope?.Database; - - if (database is null) - { - throw new InvalidOperationException("A scope is required to query the database"); - } + IUmbracoDatabase? database = _scopeAccessor.AmbientScope?.Database + ?? throw new InvalidOperationException("A scope is required to query the database"); Sql selectQuery = database.SqlContext.Sql() .SelectAll() .From("n") .Where(n => n.UniqueId == contentTypeKey, "n") - .Where(n => NodeObjectTypes.Contains(n.NodeObjectType), "n"); + .WhereIn(n => n.NodeObjectType, _nodeObjectTypes, "n"); Sql hasValuesQuery = database.SqlContext.Sql() .SelectAnyIfExists(selectQuery); return Task.FromResult(database.ExecuteScalar(hasValuesQuery)); } - - } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/HealthCheck/ExecuteActionHealthCheckControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/HealthCheck/ExecuteActionHealthCheckControllerTests.cs index 249710ab1ecb..e4dd67d64dc0 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/HealthCheck/ExecuteActionHealthCheckControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/HealthCheck/ExecuteActionHealthCheckControllerTests.cs @@ -27,7 +27,7 @@ public async Task Setup() protected override UserGroupAssertionModel AdminUserGroupAssertionModel => new() { - ExpectedStatusCode = HttpStatusCode.InternalServerError + ExpectedStatusCode = HttpStatusCode.OK }; protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new() @@ -58,7 +58,7 @@ public async Task Setup() protected override async Task ClientRequest() { HealthCheckActionRequestModel healthCheckActionRequest = - new() { HealthCheck = new ReferenceByIdModel(_dataIntegrityHealthCheckId), ValueRequired = false }; + new() { HealthCheck = new ReferenceByIdModel(_dataIntegrityHealthCheckId), ValueRequired = false, Alias = "fixContentPaths" }; return await Client.PostAsync(Url, JsonContent.Create(healthCheckActionRequest)); } } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/AllLogViewerControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/AllLogViewerControllerTests.cs index a5e4a2b4ebce..9e8c1844aea2 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/AllLogViewerControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/AllLogViewerControllerTests.cs @@ -12,7 +12,7 @@ public class AllLogViewerControllerTests : ManagementApiUserGroupTestBase new() { - ExpectedStatusCode = HttpStatusCode.InternalServerError + ExpectedStatusCode = HttpStatusCode.OK }; protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new() diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/AllMessageTemplateLogViewerControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/AllMessageTemplateLogViewerControllerTests.cs index 71c5d12ebfe6..71df00279432 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/AllMessageTemplateLogViewerControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/AllMessageTemplateLogViewerControllerTests.cs @@ -11,7 +11,7 @@ public class AllMessageTemplateLogViewerControllerTests : ManagementApiUserGroup // We get the InternalServerError for the admin because it has access, but there is no log file to view protected override UserGroupAssertionModel AdminUserGroupAssertionModel => new() { - ExpectedStatusCode = HttpStatusCode.InternalServerError + ExpectedStatusCode = HttpStatusCode.OK }; protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new() diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/LogLevelCountLogViewerControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/LogLevelCountLogViewerControllerTests.cs index 96efcdb4ee98..4fd81272814e 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/LogLevelCountLogViewerControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/LogLevelCountLogViewerControllerTests.cs @@ -11,7 +11,7 @@ public class LogLevelCountLogViewerControllerTests : ManagementApiUserGroupTestB // We get the InternalServerError for the admin because it has access, but there is no log file to view protected override UserGroupAssertionModel AdminUserGroupAssertionModel => new() { - ExpectedStatusCode = HttpStatusCode.InternalServerError + ExpectedStatusCode = HttpStatusCode.OK }; protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new() diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/ValidateLogFileSizeLogViewerControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/ValidateLogFileSizeLogViewerControllerTests.cs index fcd26afd5a77..56622b6db678 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/ValidateLogFileSizeLogViewerControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/LogViewer/ValidateLogFileSizeLogViewerControllerTests.cs @@ -11,7 +11,7 @@ public class ValidateLogFileSizeLogViewerControllerTests: ManagementApiUserGroup // We get the InternalServerError for the admin because it has access, but there is no log file to view protected override UserGroupAssertionModel AdminUserGroupAssertionModel => new() { - ExpectedStatusCode = HttpStatusCode.InternalServerError + ExpectedStatusCode = HttpStatusCode.OK }; protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new() diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/PropertyType/IsUsedPropertyTypeControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/PropertyType/IsUsedPropertyTypeControllerTests.cs index f9a70160197b..c689ad242e57 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/PropertyType/IsUsedPropertyTypeControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/PropertyType/IsUsedPropertyTypeControllerTests.cs @@ -36,7 +36,7 @@ public async Task Setup() protected override UserGroupAssertionModel AdminUserGroupAssertionModel => new() { - ExpectedStatusCode = HttpStatusCode.InternalServerError + ExpectedStatusCode = HttpStatusCode.OK }; protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new() diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/User/InviteUserControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/User/InviteUserControllerTests.cs index de91676ec3b7..827e958349f9 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/User/InviteUserControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/User/InviteUserControllerTests.cs @@ -27,7 +27,7 @@ public async Task SetUp() protected override UserGroupAssertionModel AdminUserGroupAssertionModel => new() { - ExpectedStatusCode = HttpStatusCode.InternalServerError, + ExpectedStatusCode = HttpStatusCode.InternalServerError, // We expect an error here because email sending is not configured in these tests. }; protected override UserGroupAssertionModel EditorUserGroupAssertionModel => new() diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs index 42aba90eb2f3..e8101d94140f 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using NUnit.Framework; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; @@ -11,6 +10,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing; public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest { + protected const string TextpageContentTypeKey = "1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"; + protected const string TextpageKey = "B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"; protected const string SubPageKey = "07EABF4A-5C62-4662-9F2A-15BBB488BCA5"; protected const string SubPage2Key = "0EED78FC-A6A8-4587-AB18-D3AFE212B1C4"; @@ -48,7 +49,7 @@ public virtual void CreateTestData() // Create and Save ContentType "umbTextpage" -> 1051 (template), 1052 (content type) ContentType = ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); - ContentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); + ContentType.Key = new Guid(TextpageContentTypeKey); ContentTypeService.Save(ContentType); // Create and Save Content "Homepage" based on "umbTextpage" -> 1053 diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/PropertyTypeUsageServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/PropertyTypeUsageServiceTests.cs new file mode 100644 index 000000000000..1905e2391292 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/PropertyTypeUsageServiceTests.cs @@ -0,0 +1,26 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +internal sealed class PropertyTypeUsageServiceTests : UmbracoIntegrationTestWithContent +{ + private IPropertyTypeUsageService PropertyTypeUsageService => GetRequiredService(); + + [TestCase(TextpageContentTypeKey, "title", true, true, PropertyTypeOperationStatus.Success)] + [TestCase("1D3A8E6E-2EA9-4CC1-B229-1AEE19821523", "title", false, false, PropertyTypeOperationStatus.ContentTypeNotFound)] + [TestCase(TextpageContentTypeKey, "missingProperty", true, false, PropertyTypeOperationStatus.Success)] + public async Task Can_Check_For_Saved_Property_Values(Guid contentTypeKey, string propertyAlias, bool expectedSuccess, bool expectedResult, PropertyTypeOperationStatus expectedOperationStatus) + { + Attempt resultAttempt = await PropertyTypeUsageService.HasSavedPropertyValuesAsync(contentTypeKey, propertyAlias); + Assert.AreEqual(expectedSuccess, resultAttempt.Success); + Assert.AreEqual(expectedResult, resultAttempt.Result); + Assert.AreEqual(expectedOperationStatus, resultAttempt.Status); + } +} From 41582de9d1f41f832f27e3b0adccf110238b24e4 Mon Sep 17 00:00:00 2001 From: NguyenThuyLan <116753400+NguyenThuyLan@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:46:54 +0700 Subject: [PATCH 134/155] Collection view: add tests for create and using collection view (#20667) * write test for custom collection view test * add clean * Update tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CollectionView.spec.ts Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> --------- Co-authored-by: Lan Nguyen Thuy Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> --- .../my-collection-view/my-collection-view.js | 217 ++++++++++++++++++ .../my-collection-view.js.map | 1 + .../my-collection-view/umbraco-package.json | 24 ++ .../ExtensionRegistry/CollectionView.spec.ts | 71 ++++++ 4 files changed, 313 insertions(+) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js.map create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/umbraco-package.json create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CollectionView.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js new file mode 100644 index 000000000000..38ef5a40de09 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js @@ -0,0 +1,217 @@ +import { UMB_DOCUMENT_COLLECTION_CONTEXT as T, UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN as P } from "@umbraco-cms/backoffice/document"; +import { css as D, state as d, customElement as x, html as n } from "@umbraco-cms/backoffice/external/lit"; +import { UmbLitElement as E } from "@umbraco-cms/backoffice/lit-element"; +import { UmbTextStyles as A } from "@umbraco-cms/backoffice/style"; +import { fromCamelCase as O } from "@umbraco-cms/backoffice/utils"; +var S = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, y = (e) => { + throw TypeError(e); +}, c = (e, t, a, s) => { + for (var r = s > 1 ? void 0 : s ? $(t, a) : t, p = e.length - 1, m; p >= 0; p--) + (m = e[p]) && (r = (s ? m(t, a, r) : m(r)) || r); + return s && r && S(t, a, r), r; +}, f = (e, t, a) => t.has(e) || y("Cannot " + a), u = (e, t, a) => (f(e, t, "read from private field"), t.get(e)), _ = (e, t, a) => t.has(e) ? y("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(e) : t.set(e, a), I = (e, t, a, s) => (f(e, t, "write to private field"), t.set(e, a), a), h = (e, t, a) => (f(e, t, "access private method"), a), b, o, l, w, g, v, C; +let i = class extends E { + constructor() { + super(), _(this, l), this._tableColumns = [], _(this, b, [ + { + name: this.localize.term("general_name"), + alias: "name", + elementName: "umb-document-table-column-name", + allowSorting: !0 + }, + { + name: this.localize.term("content_publishStatus"), + alias: "state", + elementName: "umb-document-table-column-state", + allowSorting: !1 + } + ]), this._tableItems = [], _(this, o), this.consumeContext(T, (e) => { + I(this, o, e), e?.setupView(this), this.observe( + e?.workspacePathBuilder, + (t) => { + this._workspacePathBuilder = t, u(this, o) && h(this, l, v).call(this, u(this, o).getItems()); + }, + "observePath" + ), h(this, l, w).call(this); + }); + } + render() { + return n` + + + + ${this._tableColumns.map( + (e) => n`` + )} + + + + ${this._tableItems.map( + (e) => n` + + ${this._tableColumns.map((t) => { + const s = e.data.find((r) => r.columnAlias === t.alias)?.value ?? ""; + return t.alias === "name" && s?.item ? n`` : t.alias === "state" && s?.item ? n`` : t.alias === "entityActions" ? n`` : n``; + })} + + ` + )} + +
    ${e.name}
    ${s.item.name}${s.item.state}${s}
    `; + } +}; +b = /* @__PURE__ */ new WeakMap(); +o = /* @__PURE__ */ new WeakMap(); +l = /* @__PURE__ */ new WeakSet(); +w = function() { + u(this, o) && (this.observe( + u(this, o).userDefinedProperties, + (e) => { + this._userDefinedProperties = e, h(this, l, g).call(this); + }, + "_observeUserDefinedProperties" + ), this.observe( + u(this, o).items, + (e) => { + this._items = e, h(this, l, v).call(this, this._items); + }, + "_observeItems" + )); +}; +g = function() { + if (this._userDefinedProperties && this._userDefinedProperties.length > 0) { + const e = this._userDefinedProperties.map((t) => ({ + name: this.localize.string(t.header), + alias: t.alias, + elementName: t.elementName, + labelTemplate: t.nameTemplate, + allowSorting: !0 + })); + this._tableColumns = [ + ...u(this, b), + ...e, + { name: "", alias: "entityActions", align: "right" } + ]; + } +}; +v = function(e) { + this._tableItems = e.map((t) => { + if (!t.unique) throw new Error("Item id is missing."); + const a = this._tableColumns?.map((s) => { + if (s.alias === "entityActions") + return { + columnAlias: "entityActions", + value: n`` + }; + const r = t.unique && this._workspacePathBuilder ? this._workspacePathBuilder({ entityType: t.entityType }) + P.generateLocal({ + unique: t.unique + }) : ""; + return { + columnAlias: s.alias, + value: s.elementName ? { item: t, editPath: r } : h(this, l, C).call(this, t, s.alias) + }; + }) ?? []; + return { + id: t.unique, + icon: t.documentType.icon, + entityType: "document", + data: a + }; + }); +}; +C = function(e, t) { + switch (t) { + case "contentTypeAlias": + return e.contentTypeAlias; + case "createDate": + return e.createDate.toLocaleString(); + case "creator": + case "owner": + return e.creator; + case "name": + return e.name; + case "state": + return O(e.state); + case "published": + return e.state !== "Draft" ? "True" : "False"; + case "sortOrder": + return e.sortOrder; + case "updateDate": + return e.updateDate.toLocaleString(); + case "updater": + return e.updater; + default: + return e.values.find((a) => a.alias === t)?.value ?? ""; + } +}; +i.styles = [ + A, + D` + :host { + display: block; + box-sizing: border-box; + height: auto; + width: 100%; + padding: var(--uui-size-space-3) 0; + } + + .container { + display: flex; + justify-content: center; + align-items: center; + } + + :host { + display: block; + width: 100%; + overflow-x: auto; + } + table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + } + th, + td { + padding: 6px 10px; + border: 1px solid #ddd; + white-space: nowrap; + } + th { + background: #f8f8f8; + font-weight: 600; + } + a { + color: var(--uui-color-interactive, #0366d6); + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + ` +]; +c([ + d() +], i.prototype, "_workspacePathBuilder", 2); +c([ + d() +], i.prototype, "_userDefinedProperties", 2); +c([ + d() +], i.prototype, "_items", 2); +c([ + d() +], i.prototype, "_tableColumns", 2); +c([ + d() +], i.prototype, "_tableItems", 2); +i = c([ + x("my-document-table-collection-view") +], i); +const z = i; +export { + i as MyDocumentTableCollectionViewElement, + z as default +}; +//# sourceMappingURL=my-collection-view.js.map diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js.map b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js.map new file mode 100644 index 000000000000..b613f3d7c86c --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js.map @@ -0,0 +1 @@ +{"version":3,"file":"my-collection-view.js","sources":["../../my-collection-view/src/my-collection-view.ts"],"sourcesContent":["import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/document';\r\nimport type { UmbDocumentCollectionItemModel } from '@umbraco-cms/backoffice/document';\r\nimport { UMB_DOCUMENT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/document';\r\nimport type { UmbCollectionColumnConfiguration } from '@umbraco-cms/backoffice/collection';\r\nimport { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';\r\nimport { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';\r\nimport { UmbTextStyles } from '@umbraco-cms/backoffice/style';\r\nimport type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';\r\nimport { fromCamelCase } from '@umbraco-cms/backoffice/utils';\r\nimport type {\r\n\tUmbTableColumn,\r\n\tUmbTableItem,\r\n} from '@umbraco-cms/backoffice/components';\r\n\r\n@customElement('my-document-table-collection-view')\r\nexport class MyDocumentTableCollectionViewElement extends UmbLitElement {\r\n\t@state()\r\n\tprivate _workspacePathBuilder?: UmbModalRouteBuilder;\r\n\r\n\t@state()\r\n\tprivate _userDefinedProperties?: Array;\r\n\r\n\t@state()\r\n\tprivate _items?: Array;\r\n\r\n\t@state()\r\n\tprivate _tableColumns: Array = [];\r\n\r\n\t#systemColumns: Array = [\r\n\t\t{\r\n\t\t\tname: this.localize.term('general_name'),\r\n\t\t\talias: 'name',\r\n\t\t\telementName: 'umb-document-table-column-name',\r\n\t\t\tallowSorting: true,\r\n\t\t},\r\n\t\t{\r\n\t\t\tname: this.localize.term('content_publishStatus'),\r\n\t\t\talias: 'state',\r\n\t\t\telementName: 'umb-document-table-column-state',\r\n\t\t\tallowSorting: false,\r\n\t\t},\r\n\t];\r\n\r\n\t@state()\r\n\tprivate _tableItems: Array = [];\r\n\r\n\t#collectionContext?: typeof UMB_DOCUMENT_COLLECTION_CONTEXT.TYPE;\r\n\r\n\tconstructor() {\r\n\t\tsuper();\r\n\r\n\t\tthis.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {\r\n\t\t\tthis.#collectionContext = collectionContext;\r\n\t\t\tcollectionContext?.setupView(this);\r\n\t\t\tthis.observe(\r\n\t\t\t\tcollectionContext?.workspacePathBuilder,\r\n\t\t\t\t(builder) => {\r\n\t\t\t\t\tthis._workspacePathBuilder = builder;\r\n\t\t\t\t\tif (this.#collectionContext) {\r\n\t\t\t\t\t\tthis.#createTableItems(this.#collectionContext.getItems());\r\n\t\t\t\t\t}\r\n\t\t\t\t},\r\n\t\t\t\t'observePath',\r\n\t\t\t);\r\n\t\t\tthis.#observeCollectionContext();\r\n\t\t});\r\n\t}\r\n\r\n\t#observeCollectionContext() {\r\n\t\tif (!this.#collectionContext) return;\r\n\r\n\t\tthis.observe(\r\n\t\t\tthis.#collectionContext.userDefinedProperties,\r\n\t\t\t(userDefinedProperties) => {\r\n\t\t\t\tthis._userDefinedProperties = userDefinedProperties;\r\n\t\t\t\tthis.#createTableHeadings();\r\n\t\t\t},\r\n\t\t\t'_observeUserDefinedProperties',\r\n\t\t);\r\n\r\n\t\tthis.observe(\r\n\t\t\tthis.#collectionContext.items,\r\n\t\t\t(items) => {\r\n\t\t\t\tthis._items = items;\r\n\t\t\t\tthis.#createTableItems(this._items);\r\n\t\t\t},\r\n\t\t\t'_observeItems',\r\n\t\t);\r\n\t}\r\n\r\n\t#createTableHeadings() {\r\n\t\tif (this._userDefinedProperties && this._userDefinedProperties.length > 0) {\r\n\t\t\tconst userColumns: Array = this._userDefinedProperties.map((item) => {\r\n\t\t\t\treturn {\r\n\t\t\t\t\tname: this.localize.string(item.header),\r\n\t\t\t\t\talias: item.alias,\r\n\t\t\t\t\telementName: item.elementName,\r\n\t\t\t\t\tlabelTemplate: item.nameTemplate,\r\n\t\t\t\t\tallowSorting: true,\r\n\t\t\t\t};\r\n\t\t\t});\r\n\r\n\t\t\tthis._tableColumns = [\r\n\t\t\t\t...this.#systemColumns,\r\n\t\t\t\t...userColumns,\r\n\t\t\t\t{ name: '', alias: 'entityActions', align: 'right' },\r\n\t\t\t];\r\n\t\t}\r\n\t}\r\n\r\n\t#createTableItems(items: Array) {\r\n\t\tthis._tableItems = items.map((item) => {\r\n\t\t\tif (!item.unique) throw new Error('Item id is missing.');\r\n\r\n\t\t\tconst data =\r\n\t\t\t\tthis._tableColumns?.map((column) => {\r\n\t\t\t\t\tif (column.alias === 'entityActions') {\r\n\t\t\t\t\t\treturn {\r\n\t\t\t\t\t\t\tcolumnAlias: 'entityActions',\r\n\t\t\t\t\t\t\tvalue: html``,\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tconst editPath =\r\n\t\t\t\t\t\titem.unique && this._workspacePathBuilder\r\n\t\t\t\t\t\t\t? this._workspacePathBuilder({ entityType: item.entityType }) +\r\n\t\t\t\t\t\t\tUMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({\r\n\t\t\t\t\t\t\t\tunique: item.unique,\r\n\t\t\t\t\t\t\t})\r\n\t\t\t\t\t\t\t: '';\r\n\r\n\t\t\t\t\treturn {\r\n\t\t\t\t\t\tcolumnAlias: column.alias,\r\n\t\t\t\t\t\tvalue: column.elementName ? { item, editPath } : this.#getPropertyValueByAlias(item, column.alias),\r\n\t\t\t\t\t};\r\n\t\t\t\t}) ?? [];\r\n\r\n\t\t\treturn {\r\n\t\t\t\tid: item.unique,\r\n\t\t\t\ticon: item.documentType.icon,\r\n\t\t\t\tentityType: 'document',\r\n\t\t\t\tdata: data,\r\n\t\t\t};\r\n\t\t});\r\n\t}\r\n\r\n\t#getPropertyValueByAlias(item: UmbDocumentCollectionItemModel, alias: string) {\r\n\tswitch (alias) {\r\n\t\tcase 'contentTypeAlias':\r\n\t\t\treturn item.contentTypeAlias;\r\n\t\tcase 'createDate':\r\n\t\t\treturn item.createDate.toLocaleString();\r\n\t\tcase 'creator':\r\n\t\tcase 'owner':\r\n\t\t\treturn item.creator;\r\n\t\tcase 'name':\r\n\t\t\treturn item.name;\r\n\t\tcase 'state':\r\n\t\t\treturn fromCamelCase(item.state);\r\n\t\tcase 'published':\r\n\t\t\treturn item.state !== 'Draft' ? 'True' : 'False';\r\n\t\tcase 'sortOrder':\r\n\t\t\treturn item.sortOrder;\r\n\t\tcase 'updateDate':\r\n\t\t\treturn item.updateDate.toLocaleString();\r\n\t\tcase 'updater':\r\n\t\t\treturn item.updater;\r\n\t\tdefault:\r\n\t\t\treturn item.values.find((value) => value.alias === alias)?.value ?? '';\r\n\t}\r\n}\r\n\r\n\toverride render() {\r\n\t\treturn html`\r\n\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t${this._tableColumns.map(\r\n\t\t\t\t\t\t\t(col) => html``,\r\n\t\t\t\t\t\t)}\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t${this._tableItems.map(\r\n\t\t\t\t\t\t\t(item) => html`\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t${this._tableColumns.map((col) => {\r\n\t\t\t\t\t\t\t\tconst cell = item.data.find((d) => d.columnAlias === col.alias);\r\n\t\t\t\t\t\t\t\tconst val = cell?.value ?? '';\r\n\t\t\t\t\t\t\t\tif (col.alias === 'name' && val?.item) {\r\n\t\t\t\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\tif (col.alias === 'state' && val?.item) {\r\n\t\t\t\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\tif (col.alias === 'entityActions') {\r\n\t\t\t\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\t\t\t})}\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t`,\r\n\t\t\t\t\t\t)}\r\n\t\t\t\t\r\n\t\t\t
    ${col.name}
    ${val.item.name}${val.item.state}${val}
    `;\r\n\t}\r\n\r\n\tstatic override styles = [\r\n\t\tUmbTextStyles,\r\n\t\tcss`\r\n\t\t\t:host {\r\n\t\t\t\tdisplay: block;\r\n\t\t\t\tbox-sizing: border-box;\r\n\t\t\t\theight: auto;\r\n\t\t\t\twidth: 100%;\r\n\t\t\t\tpadding: var(--uui-size-space-3) 0;\r\n\t\t\t}\r\n\r\n\t\t\t.container {\r\n\t\t\t\tdisplay: flex;\r\n\t\t\t\tjustify-content: center;\r\n\t\t\t\talign-items: center;\r\n\t\t\t}\r\n\r\n\t\t\t:host {\r\n\t\t\tdisplay: block;\r\n\t\t\twidth: 100%;\r\n\t\t\toverflow-x: auto;\r\n\t\t\t}\r\n\t\t\ttable {\r\n\t\t\t\twidth: 100%;\r\n\t\t\t\tborder-collapse: collapse;\r\n\t\t\t\tfont-size: 14px;\r\n\t\t\t}\r\n\t\t\tth,\r\n\t\t\ttd {\r\n\t\t\t\tpadding: 6px 10px;\r\n\t\t\t\tborder: 1px solid #ddd;\r\n\t\t\t\twhite-space: nowrap;\r\n\t\t\t}\r\n\t\t\tth {\r\n\t\t\t\tbackground: #f8f8f8;\r\n\t\t\t\tfont-weight: 600;\r\n\t\t\t}\r\n\t\t\ta {\r\n\t\t\t\tcolor: var(--uui-color-interactive, #0366d6);\r\n\t\t\t\ttext-decoration: none;\r\n\t\t\t}\r\n\t\t\ta:hover {\r\n\t\t\t\ttext-decoration: underline;\r\n\t\t\t}\r\n\t\t\t`,\r\n\t];\r\n}\r\n\r\nexport default MyDocumentTableCollectionViewElement;\r\n\r\ndeclare global {\r\n\tinterface HTMLElementTagNameMap {\r\n\t\t'my-document-table-collection-view': MyDocumentTableCollectionViewElement;\r\n\t}\r\n}\r\n"],"names":["_systemColumns","_collectionContext","_MyDocumentTableCollectionViewElement_instances","observeCollectionContext_fn","createTableHeadings_fn","createTableItems_fn","getPropertyValueByAlias_fn","MyDocumentTableCollectionViewElement","UmbLitElement","__privateAdd","UMB_DOCUMENT_COLLECTION_CONTEXT","collectionContext","__privateSet","builder","__privateGet","__privateMethod","html","col","item","val","d","userDefinedProperties","items","userColumns","data","column","editPath","UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN","alias","fromCamelCase","value","UmbTextStyles","css","__decorateClass","state","customElement","MyDocumentTableCollectionViewElement$1"],"mappings":";;;;;;;;;;;wXAAAA,GAAAC,GAAAC,GAAAC,GAAAC,GAAAC,GAAAC;AAeO,IAAMC,IAAN,cAAmDC,EAAc;AAAA,EAiCvE,cAAc;AACb,UAAA,GAlCKC,EAAA,MAAAP,CAAA,GAWN,KAAQ,gBAAuC,CAAA,GAE/CO,EAAA,MAAAT,GAAwC;AAAA,MACvC;AAAA,QACC,MAAM,KAAK,SAAS,KAAK,cAAc;AAAA,QACvC,OAAO;AAAA,QACP,aAAa;AAAA,QACb,cAAc;AAAA,MAAA;AAAA,MAEf;AAAA,QACC,MAAM,KAAK,SAAS,KAAK,uBAAuB;AAAA,QAChD,OAAO;AAAA,QACP,aAAa;AAAA,QACb,cAAc;AAAA,MAAA;AAAA,IACf,CACD,GAGA,KAAQ,cAAmC,CAAA,GAE3CS,EAAA,MAAAR,CAAA,GAKC,KAAK,eAAeS,GAAiC,CAACC,MAAsB;AAC3E,MAAAC,EAAA,MAAKX,GAAqBU,CAAA,GAC1BA,GAAmB,UAAU,IAAI,GACjC,KAAK;AAAA,QACJA,GAAmB;AAAA,QACnB,CAACE,MAAY;AACZ,eAAK,wBAAwBA,GACzBC,QAAKb,CAAA,KACRc,EAAA,MAAKb,GAAAG,CAAA,EAAL,KAAA,MAAuBS,EAAA,MAAKb,CAAA,EAAmB,SAAA,CAAS;AAAA,QAE1D;AAAA,QACA;AAAA,MAAA,GAEDc,EAAA,MAAKb,GAAAC,CAAA,EAAL,KAAA,IAAA;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EA2GS,SAAS;AACjB,WAAOa;AAAA;AAAA;AAAA;AAAA,QAID,KAAK,cAAc;AAAA,MACpB,CAACC,MAAQD,0BAA6BC,EAAI,SAAS,MAAM,KAAKA,EAAI,IAAI;AAAA,IAAA,CACtE;AAAA;AAAA;AAAA;AAAA,OAIA,KAAK,YAAY;AAAA,MACjB,CAACC,MAASF;AAAA;AAAA,UAEP,KAAK,cAAc,IAAI,CAACC,MAAQ;AAElC,cAAME,IADOD,EAAK,KAAK,KAAK,CAACE,MAAMA,EAAE,gBAAgBH,EAAI,KAAK,GAC5C,SAAS;AAC3B,eAAIA,EAAI,UAAU,UAAUE,GAAK,OACzBH,gBAAmBG,EAAI,YAAY,GAAG,IAAIA,EAAI,KAAK,IAAI,cAE3DF,EAAI,UAAU,WAAWE,GAAK,OAC1BH,QAAWG,EAAI,KAAK,KAAK,UAE7BF,EAAI,UAAU,kBACVD,0CAEDA,QAAWG,CAAG;AAAA,MACtB,CAAC,CAAC;AAAA;AAAA;AAAA,IAAA,CAGF;AAAA;AAAA;AAAA,EAGN;AAgDD;AAlOCnB,IAAA,oBAAA,QAAA;AAkBAC,IAAA,oBAAA,QAAA;AA/BMC,IAAA,oBAAA,QAAA;AAqDNC,IAAyB,WAAG;AAC3B,EAAKW,QAAKb,CAAA,MAEV,KAAK;AAAA,IACJa,QAAKb,CAAA,EAAmB;AAAA,IACxB,CAACoB,MAA0B;AAC1B,WAAK,yBAAyBA,GAC9BN,EAAA,MAAKb,GAAAE,CAAA,EAAL,KAAA,IAAA;AAAA,IACD;AAAA,IACA;AAAA,EAAA,GAGD,KAAK;AAAA,IACJU,QAAKb,CAAA,EAAmB;AAAA,IACxB,CAACqB,MAAU;AACV,WAAK,SAASA,GACdP,EAAA,MAAKb,GAAAG,CAAA,EAAL,WAAuB,KAAK,MAAA;AAAA,IAC7B;AAAA,IACA;AAAA,EAAA;AAEF;AAEAD,IAAoB,WAAG;AACtB,MAAI,KAAK,0BAA0B,KAAK,uBAAuB,SAAS,GAAG;AAC1E,UAAMmB,IAAqC,KAAK,uBAAuB,IAAI,CAACL,OACpE;AAAA,MACN,MAAM,KAAK,SAAS,OAAOA,EAAK,MAAM;AAAA,MACtC,OAAOA,EAAK;AAAA,MACZ,aAAaA,EAAK;AAAA,MAClB,eAAeA,EAAK;AAAA,MACpB,cAAc;AAAA,IAAA,EAEf;AAED,SAAK,gBAAgB;AAAA,MACpB,GAAGJ,EAAA,MAAKd,CAAA;AAAA,MACR,GAAGuB;AAAA,MACH,EAAE,MAAM,IAAI,OAAO,iBAAiB,OAAO,QAAA;AAAA,IAAQ;AAAA,EAErD;AACD;AAEAlB,IAAiB,SAACiB,GAA8C;AAC/D,OAAK,cAAcA,EAAM,IAAI,CAACJ,MAAS;AACtC,QAAI,CAACA,EAAK,OAAQ,OAAM,IAAI,MAAM,qBAAqB;AAEvD,UAAMM,IACL,KAAK,eAAe,IAAI,CAACC,MAAW;AACnC,UAAIA,EAAO,UAAU;AACpB,eAAO;AAAA,UACN,aAAa;AAAA,UACb,OAAOT;AAAA,iBACGE,CAAI;AAAA,QAAA;AAIhB,YAAMQ,IACLR,EAAK,UAAU,KAAK,wBACjB,KAAK,sBAAsB,EAAE,YAAYA,EAAK,WAAA,CAAY,IAC5DS,EAAyC,cAAc;AAAA,QACtD,QAAQT,EAAK;AAAA,MAAA,CACb,IACC;AAEJ,aAAO;AAAA,QACN,aAAaO,EAAO;AAAA,QACpB,OAAOA,EAAO,cAAc,EAAE,MAAAP,GAAM,UAAAQ,EAAA,IAAaX,EAAA,MAAKb,GAAAI,CAAA,EAAL,KAAA,MAA8BY,GAAMO,EAAO,KAAA;AAAA,MAAA;AAAA,IAE9F,CAAC,KAAK,CAAA;AAEP,WAAO;AAAA,MACN,IAAIP,EAAK;AAAA,MACT,MAAMA,EAAK,aAAa;AAAA,MACxB,YAAY;AAAA,MACZ,MAAAM;AAAA,IAAA;AAAA,EAEF,CAAC;AACF;AAEAlB,IAAwB,SAACY,GAAsCU,GAAe;AAC9E,UAAQA,GAAA;AAAA,IACP,KAAK;AACJ,aAAOV,EAAK;AAAA,IACb,KAAK;AACJ,aAAOA,EAAK,WAAW,eAAA;AAAA,IACxB,KAAK;AAAA,IACL,KAAK;AACJ,aAAOA,EAAK;AAAA,IACb,KAAK;AACJ,aAAOA,EAAK;AAAA,IACb,KAAK;AACJ,aAAOW,EAAcX,EAAK,KAAK;AAAA,IAChC,KAAK;AACJ,aAAOA,EAAK,UAAU,UAAU,SAAS;AAAA,IAC1C,KAAK;AACJ,aAAOA,EAAK;AAAA,IACb,KAAK;AACJ,aAAOA,EAAK,WAAW,eAAA;AAAA,IACxB,KAAK;AACJ,aAAOA,EAAK;AAAA,IACb;AACC,aAAOA,EAAK,OAAO,KAAK,CAACY,MAAUA,EAAM,UAAUF,CAAK,GAAG,SAAS;AAAA,EAAA;AAEvE;AA5JarB,EAiMI,SAAS;AAAA,EACxBwB;AAAA,EACAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2CD;AA5OQC,EAAA;AAAA,EADPC,EAAA;AAAM,GADK3B,EAEJ,WAAA,yBAAA,CAAA;AAGA0B,EAAA;AAAA,EADPC,EAAA;AAAM,GAJK3B,EAKJ,WAAA,0BAAA,CAAA;AAGA0B,EAAA;AAAA,EADPC,EAAA;AAAM,GAPK3B,EAQJ,WAAA,UAAA,CAAA;AAGA0B,EAAA;AAAA,EADPC,EAAA;AAAM,GAVK3B,EAWJ,WAAA,iBAAA,CAAA;AAkBA0B,EAAA;AAAA,EADPC,EAAA;AAAM,GA5BK3B,EA6BJ,WAAA,eAAA,CAAA;AA7BIA,IAAN0B,EAAA;AAAA,EADNE,EAAc,mCAAmC;AAAA,GACrC5B,CAAA;AAiPb,MAAA6B,IAAe7B;"} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/umbraco-package.json b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/umbraco-package.json new file mode 100644 index 000000000000..7cbeb21c79e5 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/umbraco-package.json @@ -0,0 +1,24 @@ +{ + "$schema": "../../umbraco-package-schema.json", + "name": "My Collection View", + "version": "0.1.0", + "extensions": [ + { + "type": "collectionView", + "alias": "My.CollectionView.Document.Table", + "name": "My Collection View Table", + "element": "/App_Plugins/my-collection-view/my-collection-view.js", + "meta": { + "label": "Table", + "icon": "icon-list", + "pathName": "table" + }, + "conditions": [ + { + "alias": "Umb.Condition.CollectionAlias", + "match": "Umb.Collection.Document" + } + ] + } + ] +} diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CollectionView.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CollectionView.spec.ts new file mode 100644 index 000000000000..8ebda107cfef --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CollectionView.spec.ts @@ -0,0 +1,71 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +// Content +const parentContentName = 'parentContentName'; +// DocumentType +const documentTypeParentName = 'ParentDocumentType'; +const documentTypeChildName = 'ChildDocumentType'; +// DataType +const customDataTypeName = 'Custom List View'; +const layoutName = 'My Collection View Table'; +const layoutCollectionView = 'My.CollectionView.Document.Table'; + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(parentContentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeParentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeChildName); + await umbracoApi.dataType.ensureNameNotExists(customDataTypeName); +}); + +test('can see the custom collection view when choosing layout for new collection data type', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); + await umbracoApi.dataType.createListViewContentDataType(customDataTypeName); + await umbracoUi.dataType.goToDataType(customDataTypeName); + + // Act + await umbracoUi.dataType.addLayouts(layoutName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessStateVisibleForSaveButton(); + expect(await umbracoApi.dataType.doesListViewHaveLayout(customDataTypeName, layoutName, 'icon-list', layoutCollectionView)).toBeTruthy(); +}); + +test('can see the pagination works when using custom collection view in content section', async ({umbracoApi, umbracoUi}) => { + // Arrange + const pageSize = 5; + const totalItems = 7; + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); + const dataTypeId = await umbracoApi.dataType.createListViewContentDataTypeWithLayoutAndPageSize(customDataTypeName,layoutCollectionView, layoutName, pageSize); + const documentTypeChildId = await umbracoApi.documentType.createDefaultDocumentType(documentTypeChildName); + const documentTypeParentId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNodeAndCollectionId(documentTypeParentName, documentTypeChildId, dataTypeId); + const documentParentId = await umbracoApi.document.createDefaultDocument(parentContentName, documentTypeParentId); + for (let i = 1; i <= totalItems; i++) { + await umbracoApi.document.createDefaultDocumentWithParent('Test child ' + i, documentTypeChildId, documentParentId); + } + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(parentContentName); + + // Assert + // Page 1 + await umbracoUi.content.doesListViewItemsHaveCount(pageSize); + await umbracoUi.content.isListViewItemWithNameVisible('Test child 1', 0); + await umbracoUi.content.isListViewItemWithNameVisible('Test child 5', 4); + // Page 2 + await umbracoUi.content.clickPaginationNextButton(); + await umbracoUi.content.doesListViewItemsHaveCount(2); + await umbracoUi.content.isListViewItemWithNameVisible('Test child 6', 0); + await umbracoUi.content.isListViewItemWithNameVisible('Test child 7', 1); + + // Clean + for (let i = 1; i <= totalItems; i++) { + await umbracoApi.document.ensureNameNotExists('Test child ' + i); + } +}); \ No newline at end of file From 524912a893a294f3f27317aa00fe02798213b612 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 11 Nov 2025 11:37:00 +0100 Subject: [PATCH 135/155] Fix accidental update to global.json. --- global.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index bfcfa560ae56..33b94ff507e3 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.306", + "version": "10.0.100-rc.2.25502.107", "rollForward": "latestFeature", "allowPrerelease": true } -} \ No newline at end of file +} From 0858d02172ae5dc5c1f633ff487ec1aab77ba310 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Tue, 11 Nov 2025 12:43:03 +0100 Subject: [PATCH 136/155] Single block migration (#20663) * WiP blocklist migration * Mostly working migration * [WIP] deconstructed the migration to prefetch and process all data that requires the old definitions * Working singleblock migration * Abstracted some logic and applied it to settings elements too. * Align class and file name. * Minor code warning resolution. * More and better comments + made classes internal where it made sense --------- Co-authored-by: Andy Butland --- .../BlockEditorElementTypeCache.cs | 10 +- .../IBlockEditorElementTypeCache.cs | 1 + .../Migrations/Upgrade/UmbracoPlan.cs | 4 + .../V_18_0_0/MigrateSingleBlockList.cs | 436 ++++++++++++++++++ .../ITypedSingleBlockListProcessor.cs | 23 + .../MigrateSingleBlockListComposer.cs | 18 + .../SingleBlockBlockProcessorBase.cs | 41 ++ .../SingleBlockListBlockGridProcessor.cs | 50 ++ .../SingleBlockListBlockListProcessor.cs | 52 +++ .../SingleBlockListConfigurationCache.cs | 52 +++ .../SingleBlockListProcessor.cs | 51 ++ .../SingleBlockListRteProcessor.cs | 58 +++ 12 files changed, 792 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/MigrateSingleBlockList.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/ITypedSingleBlockListProcessor.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/MigrateSingleBlockListComposer.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockBlockProcessorBase.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListBlockGridProcessor.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListBlockListProcessor.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListConfigurationCache.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListProcessor.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListRteProcessor.cs diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs index 14dc4db63214..f47501c94b1d 100644 --- a/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs +++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs @@ -6,9 +6,11 @@ namespace Umbraco.Cms.Core.Cache.PropertyEditors; internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache { + private const string CacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes"; private readonly IContentTypeService _contentTypeService; private readonly AppCaches _appCaches; + public BlockEditorElementTypeCache(IContentTypeService contentTypeService, AppCaches appCaches) { _contentTypeService = contentTypeService; @@ -20,15 +22,15 @@ public BlockEditorElementTypeCache(IContentTypeService contentTypeService, AppCa public IEnumerable GetAll() { // TODO: make this less dumb; don't fetch all elements, only fetch the items that aren't yet in the cache and amend the cache as more elements are loaded - - const string cacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes"; - IEnumerable? cachedElements = _appCaches.RequestCache.GetCacheItem>(cacheKey); + IEnumerable? cachedElements = _appCaches.RequestCache.GetCacheItem>(CacheKey); if (cachedElements is null) { cachedElements = _contentTypeService.GetAllElementTypes(); - _appCaches.RequestCache.Set(cacheKey, cachedElements); + _appCaches.RequestCache.Set(CacheKey, cachedElements); } return cachedElements; } + + public void ClearAll() => _appCaches.RequestCache.Remove(CacheKey); } diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs index f99d2ff8757f..48142de97ffd 100644 --- a/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs +++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs @@ -6,4 +6,5 @@ public interface IBlockEditorElementTypeCache { IEnumerable GetMany(IEnumerable keys); IEnumerable GetAll(); + void ClearAll() { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 7a781b889a46..8bdddc0a55a7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -139,5 +139,9 @@ protected virtual void DefinePlan() To("{263075BF-F18A-480D-92B4-4947D2EAB772}"); To("26179D88-58CE-4C92-B4A4-3CBA6E7188AC"); To("{8B2C830A-4FFB-4433-8337-8649B0BF52C8}"); + + // To 18.0.0 + // TODO (V18): Enable on 18 branch + //// To("{74332C49-B279-4945-8943-F8F00B1F5949}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/MigrateSingleBlockList.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/MigrateSingleBlockList.cs new file mode 100644 index 000000000000..23d683ee01e7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/MigrateSingleBlockList.cs @@ -0,0 +1,436 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; +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.PropertyEditors; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.PropertyEditors; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0; + +public class MigrateSingleBlockList : AsyncMigrationBase +{ + private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly ILanguageService _languageService; + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IDataTypeService _dataTypeService; + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly SingleBlockListProcessor _singleBlockListProcessor; + private readonly IJsonSerializer _jsonSerializer; + private readonly SingleBlockListConfigurationCache _blockListConfigurationCache; + private readonly IBlockEditorElementTypeCache _elementTypeCache; + private readonly AppCaches _appCaches; + private readonly ILogger _logger; + private readonly IDataValueEditor _dummySingleBlockValueEditor; + + public MigrateSingleBlockList( + IMigrationContext context, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IDataTypeService dataTypeService, + ILogger logger, + ICoreScopeProvider coreScopeProvider, + SingleBlockListProcessor singleBlockListProcessor, + IJsonSerializer jsonSerializer, + SingleBlockListConfigurationCache blockListConfigurationCache, + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory, + IBlockEditorElementTypeCache elementTypeCache, + AppCaches appCaches) + : base(context) + { + _umbracoContextFactory = umbracoContextFactory; + _languageService = languageService; + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _dataTypeService = dataTypeService; + _logger = logger; + _coreScopeProvider = coreScopeProvider; + _singleBlockListProcessor = singleBlockListProcessor; + _jsonSerializer = jsonSerializer; + _blockListConfigurationCache = blockListConfigurationCache; + _elementTypeCache = elementTypeCache; + _appCaches = appCaches; + + _dummySingleBlockValueEditor = new SingleBlockPropertyEditor(dataValueEditorFactory, jsonSerializer, ioHelper, blockValuePropertyIndexValueFactory).GetValueEditor(); + } + + protected override async Task MigrateAsync() + { + // gets filled by all registered ITypedSingleBlockListProcessor + IEnumerable propertyEditorAliases = _singleBlockListProcessor.GetSupportedPropertyEditorAliases(); + + using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + var languagesById = (await _languageService.GetAllAsync()) + .ToDictionary(language => language.Id); + + IEnumerable allContentTypes = _contentTypeService.GetAll(); + IEnumerable contentPropertyTypes = allContentTypes + .SelectMany(ct => ct.PropertyTypes); + + IMediaType[] allMediaTypes = _mediaTypeService.GetAll().ToArray(); + IEnumerable mediaPropertyTypes = allMediaTypes + .SelectMany(ct => ct.PropertyTypes); + + // get all relevantPropertyTypes + var relevantPropertyEditors = + contentPropertyTypes.Concat(mediaPropertyTypes).DistinctBy(pt => pt.Id) + .Where(pt => propertyEditorAliases.Contains(pt.PropertyEditorAlias)) + .GroupBy(pt => pt.PropertyEditorAlias) + .ToDictionary(group => group.Key, group => group.ToArray()); + + // populate the cache to limit amount of db locks in recursion logic. + var blockListsConfiguredAsSingleCount = await _blockListConfigurationCache.Populate(); + + if (blockListsConfiguredAsSingleCount == 0) + { + _logger.LogInformation( + "No blocklist were configured as single, nothing to do."); + return; + } + + _logger.LogInformation( + "Found {blockListsConfiguredAsSingleCount} number of blockListConfigurations with UseSingleBlockMode set to true", + blockListsConfiguredAsSingleCount); + + // we want to batch actual update calls to the database, so we are grouping them by propertyEditorAlias + // and again by propertyType(dataType). + var updateItemsByPropertyEditorAlias = new Dictionary>>(); + + // For each propertyEditor, collect and process all propertyTypes and their propertyData + foreach (var propertyEditorAlias in propertyEditorAliases) + { + if (relevantPropertyEditors.TryGetValue(propertyEditorAlias, out IPropertyType[]? propertyTypes) is false) + { + continue; + } + + _logger.LogInformation( + "Migration starting for all properties of type: {propertyEditorAlias}", + propertyEditorAlias); + Dictionary> updateItemsByPropertyType = await ProcessPropertyTypesAsync(propertyTypes, languagesById); + if (updateItemsByPropertyType.Count < 1) + { + _logger.LogInformation( + "No properties have been found to migrate for {propertyEditorAlias}", + propertyEditorAlias); + return; + } + + updateItemsByPropertyEditorAlias[propertyEditorAlias] = updateItemsByPropertyType; + } + + // update the configuration of all propertyTypes + var singleBlockListDataTypesIds = _blockListConfigurationCache.CachedDataTypes.ToList().Select(type => type.Id).ToList(); + + string updateSql = $@" +UPDATE umbracoDataType +SET propertyEditorAlias = '{Constants.PropertyEditors.Aliases.SingleBlock}', + propertyEditorUiAlias = 'Umb.PropertyEditorUi.SingleBlock' +WHERE nodeId IN (@0)"; + await Database.ExecuteAsync(updateSql, singleBlockListDataTypesIds); + + // we need to clear the elementTypeCache so the second part of the migration can work with the update dataTypes + // and also the isolated/runtime Caches as that is what its build from in the default implementation + _elementTypeCache.ClearAll(); + _appCaches.IsolatedCaches.ClearAllCaches(); + _appCaches.RuntimeCache.Clear(); + RebuildCache = true; + + // now that we have updated the configuration of all propertyTypes, we can save the updated propertyTypes + foreach (string propertyEditorAlias in updateItemsByPropertyEditorAlias.Keys) + { + if (await SavePropertyTypes(updateItemsByPropertyEditorAlias[propertyEditorAlias])) + { + _logger.LogInformation( + "Migration succeeded for all properties of type: {propertyEditorAlias}", + propertyEditorAlias); + } + else + { + _logger.LogError( + "Migration failed for one or more properties of type: {propertyEditorAlias}", + propertyEditorAlias); + } + } + } + + private async Task>> ProcessPropertyTypesAsync(IPropertyType[] propertyTypes, IDictionary languagesById) + { + var updateItemsByPropertyType = new Dictionary>(); + foreach (IPropertyType propertyType in propertyTypes) + { + // make sure the passed in data is valid and can be processed + IDataType dataType = await _dataTypeService.GetAsync(propertyType.DataTypeKey) + ?? throw new InvalidOperationException("The data type could not be fetched."); + IDataValueEditor valueEditor = dataType.Editor?.GetValueEditor() + ?? throw new InvalidOperationException( + "The data type value editor could not be obtained."); + + // fetch all the propertyData for the current propertyType + Sql sql = Sql() + .Select() + .From() + .InnerJoin() + .On((propertyData, contentVersion) => + propertyData.VersionId == contentVersion.Id) + .LeftJoin() + .On((contentVersion, documentVersion) => + contentVersion.Id == documentVersion.Id) + .Where((propertyData, contentVersion, documentVersion) => + (contentVersion.Current == true || documentVersion.Published == true) + && propertyData.PropertyTypeId == propertyType.Id); + + List propertyDataDtos = await Database.FetchAsync(sql); + if (propertyDataDtos.Count < 1) + { + continue; + } + + var updateItems = new List(); + + // process all the propertyData + // if none of the processors modify the value, the propertyData is skipped from being saved. + foreach (PropertyDataDto propertyDataDto in propertyDataDtos) + { + if (ProcessPropertyDataDto(propertyDataDto, propertyType, languagesById, valueEditor, out UpdateItem? updateItem) is false) + { + continue; + } + + updateItems.Add(updateItem!); + } + + updateItemsByPropertyType[propertyType] = updateItems; + } + + return updateItemsByPropertyType; + } + + private async Task SavePropertyTypes(IDictionary> propertyTypes) + { + foreach (IPropertyType propertyType in propertyTypes.Keys) + { + // The dataType and valueEditor should be constructed as we have done this before, but we hate null values. + IDataType dataType = await _dataTypeService.GetAsync(propertyType.DataTypeKey) + ?? throw new InvalidOperationException("The data type could not be fetched."); + IDataValueEditor updatedValueEditor = dataType.Editor?.GetValueEditor() + ?? throw new InvalidOperationException( + "The data type value editor could not be obtained."); + + // batch by datatype + var propertyDataDtos = propertyTypes[propertyType].Select(item => item.PropertyDataDto).ToList(); + + var updateBatch = propertyDataDtos.Select(propertyDataDto => + UpdateBatch.For(propertyDataDto, Database.StartSnapshot(propertyDataDto))).ToList(); + + var updatesToSkip = new ConcurrentBag>(); + + var progress = 0; + + void HandleUpdateBatch(UpdateBatch update) + { + using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + + progress++; + if (progress % 100 == 0) + { + _logger.LogInformation(" - finíshed {progress} of {total} properties", progress, updateBatch.Count); + } + + PropertyDataDto propertyDataDto = update.Poco; + + if (FinalizeUpdateItem(propertyTypes[propertyType].First(item => Equals(item.PropertyDataDto, update.Poco)), updatedValueEditor) is false) + { + updatesToSkip.Add(update); + } + } + + if (DatabaseType == DatabaseType.SQLite) + { + // SQLite locks up if we run the migration in parallel, so... let's not. + foreach (UpdateBatch update in updateBatch) + { + HandleUpdateBatch(update); + } + } + else + { + Parallel.ForEachAsync(updateBatch, async (update, token) => + { + //Foreach here, but we need to suppress the flow before each task, but not the actuall await of the task + Task task; + using (ExecutionContext.SuppressFlow()) + { + task = Task.Run( + () => + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + scope.Complete(); + HandleUpdateBatch(update); + }, + token); + } + + await task; + }).GetAwaiter().GetResult(); + } + + updateBatch.RemoveAll(updatesToSkip.Contains); + + if (updateBatch.Any() is false) + { + _logger.LogDebug(" - no properties to convert, continuing"); + continue; + } + + _logger.LogInformation(" - {totalConverted} properties converted, saving...", updateBatch.Count); + var result = Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 }); + if (result != updateBatch.Count) + { + throw new InvalidOperationException( + $"The database batch update was supposed to update {updateBatch.Count} property DTO entries, but it updated {result} entries."); + } + + _logger.LogDebug( + "Migration completed for property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias}, editor alias: {propertyTypeEditorAlias}) - {updateCount} property DTO entries updated.", + propertyType.Name, + propertyType.Id, + propertyType.Alias, + propertyType.PropertyEditorAlias, + result); + } + + return true; + } + + private bool ProcessPropertyDataDto( + PropertyDataDto propertyDataDto, + IPropertyType propertyType, + IDictionary languagesById, + IDataValueEditor valueEditor, + out UpdateItem? updateItem) + { + // NOTE: some old property data DTOs can have variance defined, even if the property type no longer varies + var culture = propertyType.VariesByCulture() + && propertyDataDto.LanguageId.HasValue + && languagesById.TryGetValue(propertyDataDto.LanguageId.Value, out ILanguage? language) + ? language.IsoCode + : null; + + if (culture is null && propertyType.VariesByCulture()) + { + // if we end up here, the property DTO is bound to a language that no longer exists. this is an error scenario, + // and we can't really handle it in any other way than logging; in all likelihood this is an old property version, + // and it won't cause any runtime issues + _logger.LogWarning( + " - property data with id: {propertyDataId} references a language that does not exist - language id: {languageId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + propertyDataDto.LanguageId, + propertyType.Name, + propertyType.Id, + propertyType.Alias); + updateItem = null; + return false; + } + + // create a fake property to be able to get a typed value and run it trough the processors. + var segment = propertyType.VariesBySegment() ? propertyDataDto.Segment : null; + var property = new Property(propertyType); + property.SetValue(propertyDataDto.Value, culture, segment); + var toEditorValue = valueEditor.ToEditor(property, culture, segment); + + if (TryTransformValue(toEditorValue, property, out var updatedValue) is false) + { + _logger.LogDebug( + " - skipping as no processor modified the data for property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + propertyType.Name, + propertyType.Id, + propertyType.Alias); + updateItem = null; + return false; + } + + updateItem = new UpdateItem(propertyDataDto, propertyType, updatedValue); + return true; + } + + /// + /// Takes the updated value that was instanced from the db value by the old ValueEditors + /// And runs it through the updated ValueEditors and sets it on the PropertyDataDto + /// + private bool FinalizeUpdateItem(UpdateItem updateItem, IDataValueEditor updatedValueEditor) + { + var editorValue = _jsonSerializer.Serialize(updateItem.UpdatedValue); + var dbValue = updateItem.UpdatedValue is SingleBlockValue + ? _dummySingleBlockValueEditor.FromEditor(new ContentPropertyData(editorValue, null), null) + : updatedValueEditor.FromEditor(new ContentPropertyData(editorValue, null), null); + if (dbValue is not string stringValue || stringValue.DetectIsJson() is false) + { + _logger.LogWarning( + " - value editor did not yield a valid JSON string as FromEditor value property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + updateItem.PropertyDataDto.Id, + updateItem.PropertyType.Name, + updateItem.PropertyType.Id, + updateItem.PropertyType.Alias); + return false; + } + + updateItem.PropertyDataDto.TextValue = stringValue; + return true; + } + + /// + /// If the value is a BlockListValue, and its datatype is configured as single + /// We also need to convert the outer BlockListValue to a SingleBlockValue + /// Either way, we need to run the value through the processors to possibly update nested values + /// + private bool TryTransformValue(object? toEditorValue, Property property, out object? value) + { + bool hasChanged = _singleBlockListProcessor.ProcessToEditorValue(toEditorValue); + + if (toEditorValue is BlockListValue blockListValue + && _blockListConfigurationCache.IsPropertyEditorBlockListConfiguredAsSingle(property.PropertyType.DataTypeKey)) + { + value = _singleBlockListProcessor.ConvertBlockListToSingleBlock(blockListValue); + return true; + } + + value = toEditorValue; + return hasChanged; + } + + private class UpdateItem + { + public UpdateItem(PropertyDataDto propertyDataDto, IPropertyType propertyType, object? updatedValue) + { + PropertyDataDto = propertyDataDto; + PropertyType = propertyType; + UpdatedValue = updatedValue; + } + + public object? UpdatedValue { get; set; } + + public PropertyDataDto PropertyDataDto { get; set; } + + public IPropertyType PropertyType { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/ITypedSingleBlockListProcessor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/ITypedSingleBlockListProcessor.cs new file mode 100644 index 000000000000..7ad85505d922 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/ITypedSingleBlockListProcessor.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; + +[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21 +public interface ITypedSingleBlockListProcessor +{ + /// + /// The type of the propertyEditor expects to receive as a value to process + /// + public Type PropertyEditorValueType { get; } + + /// + /// The property (data)editor aliases that this processor supports, as defined on their DataEditor attributes + /// + public IEnumerable PropertyEditorAliases { get; } + + /// + /// object?: the editorValue being processed + /// Func: the function that will be called when nested content is detected + /// + public Func, Func, bool> Process { get; } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/MigrateSingleBlockListComposer.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/MigrateSingleBlockListComposer.cs new file mode 100644 index 000000000000..367cb3af3d0b --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/MigrateSingleBlockListComposer.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; + +[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21 +internal class MigrateSingleBlockListComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockBlockProcessorBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockBlockProcessorBase.cs new file mode 100644 index 000000000000..ce88ef01e8b1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockBlockProcessorBase.cs @@ -0,0 +1,41 @@ +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; + +[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21 +internal abstract class SingleBlockBlockProcessorBase +{ + private readonly SingleBlockListConfigurationCache _blockListConfigurationCache; + + public SingleBlockBlockProcessorBase( + SingleBlockListConfigurationCache blockListConfigurationCache) + { + _blockListConfigurationCache = blockListConfigurationCache; + } + + protected bool ProcessBlockItemDataValues( + BlockItemData blockItemData, + Func processNested, + Func processOuterValue) + { + var hasChanged = false; + + foreach (BlockPropertyValue blockPropertyValue in blockItemData.Values) + { + if (processNested.Invoke(blockPropertyValue.Value)) + { + hasChanged = true; + } + + if (_blockListConfigurationCache.IsPropertyEditorBlockListConfiguredAsSingle( + blockPropertyValue.PropertyType!.DataTypeKey) + && blockPropertyValue.Value is BlockListValue blockListValue) + { + blockPropertyValue.Value = processOuterValue.Invoke(blockListValue); + hasChanged = true; + } + } + + return hasChanged; + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListBlockGridProcessor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListBlockGridProcessor.cs new file mode 100644 index 000000000000..f84077a47ef6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListBlockGridProcessor.cs @@ -0,0 +1,50 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; + +[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21 +internal class SingleBlockListBlockGridProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor +{ + public SingleBlockListBlockGridProcessor(SingleBlockListConfigurationCache blockListConfigurationCache) + : base(blockListConfigurationCache) + { + } + + public Type PropertyEditorValueType => typeof(BlockGridValue); + + public IEnumerable PropertyEditorAliases => [Constants.PropertyEditors.Aliases.BlockGrid]; + + public Func,Func, bool> Process => ProcessBlocks; + + private bool ProcessBlocks( + object? value, + Func processNested, + Func processOuterValue) + { + if (value is not BlockGridValue blockValue) + { + return false; + } + + bool hasChanged = false; + + foreach (BlockItemData contentData in blockValue.ContentData) + { + if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue)) + { + hasChanged = true; + } + } + + foreach (BlockItemData settingsData in blockValue.SettingsData) + { + if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue)) + { + hasChanged = true; + } + } + + return hasChanged; + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListBlockListProcessor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListBlockListProcessor.cs new file mode 100644 index 000000000000..71c7f65feb75 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListBlockListProcessor.cs @@ -0,0 +1,52 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; + +[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21 +internal class SingleBlockListBlockListProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor +{ + public SingleBlockListBlockListProcessor( + SingleBlockListConfigurationCache blockListConfigurationCache) + : base(blockListConfigurationCache) + { + } + + public Type PropertyEditorValueType => typeof(BlockListValue); + + public IEnumerable PropertyEditorAliases => [Constants.PropertyEditors.Aliases.BlockList]; + + public Func, Func, bool> Process => ProcessBlocks; + + private bool ProcessBlocks( + object? value, + Func processNested, + Func processOuterValue) + { + if (value is not BlockListValue blockValue) + { + return false; + } + + bool hasChanged = false; + + // there might be another list inside the single list so more recursion, yeeey! + foreach (BlockItemData contentData in blockValue.ContentData) + { + if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue)) + { + hasChanged = true; + } + } + + foreach (BlockItemData settingsData in blockValue.SettingsData) + { + if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue)) + { + hasChanged = true; + } + } + + return hasChanged; + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListConfigurationCache.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListConfigurationCache.cs new file mode 100644 index 000000000000..7e342b3de7a9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListConfigurationCache.cs @@ -0,0 +1,52 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; + +/// +/// Used by the SingleBlockList Migration and its processors to avoid having to fetch (and thus lock) +/// data from the db multiple times during the migration. +/// +[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21 +public class SingleBlockListConfigurationCache +{ + private readonly IDataTypeService _dataTypeService; + private readonly List _singleBlockListDataTypes = new(); + + public SingleBlockListConfigurationCache(IDataTypeService dataTypeService) + { + _dataTypeService = dataTypeService; + } + + /// + /// Populates a cache that holds all the property editor aliases that have a BlockList configuration with UseSingleBlockMode set to true. + /// + /// The number of blocklists with UseSingleBlockMode set to true. + public async Task Populate() + { + IEnumerable blockListDataTypes = + await _dataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.BlockList); + + foreach (IDataType dataType in blockListDataTypes) + { + if (dataType.ConfigurationObject is BlockListConfiguration + { + UseSingleBlockMode: true, ValidationLimit.Max: 1 + }) + { + _singleBlockListDataTypes.Add(dataType); + } + } + + return _singleBlockListDataTypes.Count; + } + + // returns whether the passed in key belongs to a blocklist with UseSingleBlockMode set to true + public bool IsPropertyEditorBlockListConfiguredAsSingle(Guid key) => + _singleBlockListDataTypes.Any(dt => dt.Key == key); + + // The list of all blocklist data types that have UseSingleBlockMode set to true + public IEnumerable CachedDataTypes => _singleBlockListDataTypes.AsReadOnly(); +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListProcessor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListProcessor.cs new file mode 100644 index 000000000000..ae13717f8f21 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListProcessor.cs @@ -0,0 +1,51 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; + +[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21 +public class SingleBlockListProcessor +{ + private readonly IEnumerable _processors; + + public SingleBlockListProcessor(IEnumerable processors) => _processors = processors; + + public IEnumerable GetSupportedPropertyEditorAliases() => + _processors.SelectMany(p => p.PropertyEditorAliases); + /// + /// The entry point of the recursive conversion + /// Find the first processor that can handle the value and call it's Process method + /// + /// Whether the value was changed + public bool ProcessToEditorValue(object? editorValue) + { + ITypedSingleBlockListProcessor? processor = + _processors.FirstOrDefault(p => p.PropertyEditorValueType == editorValue?.GetType()); + + return processor is not null && processor.Process.Invoke(editorValue, ProcessToEditorValue, ConvertBlockListToSingleBlock); + } + + /// + /// Updates and returns the passed in BlockListValue to a SingleBlockValue + /// Should only be called by a core processor once a BlockListValue has been found that is configured in single block mode. + /// + public BlockValue ConvertBlockListToSingleBlock(BlockListValue blockListValue) + { + IBlockLayoutItem blockListLayoutItem = blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].First(); + + var singleBlockLayoutItem = new SingleBlockLayoutItem + { + ContentKey = blockListLayoutItem.ContentKey, + SettingsKey = blockListLayoutItem.SettingsKey, + }; + + var singleBlockValue = new SingleBlockValue(singleBlockLayoutItem) + { + ContentData = blockListValue.ContentData, + SettingsData = blockListValue.SettingsData, + Expose = blockListValue.Expose, + }; + + return singleBlockValue; + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListRteProcessor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListRteProcessor.cs new file mode 100644 index 000000000000..88de3b839b36 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/SingleBlockList/SingleBlockListRteProcessor.cs @@ -0,0 +1,58 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList; + +[Obsolete("Will be removed in V22")] // available in v17, activated in v18 migration needs to work on LTS to LTS 17=>21 +internal class SingleBlockListRteProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor +{ + public SingleBlockListRteProcessor(SingleBlockListConfigurationCache blockListConfigurationCache) + : base(blockListConfigurationCache) + { + } + + public Type PropertyEditorValueType => typeof(RichTextEditorValue); + + public IEnumerable PropertyEditorAliases => + [ + "Umbraco.TinyMCE", Constants.PropertyEditors.Aliases.RichText + ]; + + public Func,Func, bool> Process => ProcessRichText; + + public bool ProcessRichText( + object? value, + Func processNested, + Func processOuterValue) + { + if (value is not RichTextEditorValue richTextValue) + { + return false; + } + + var hasChanged = false; + + if (richTextValue.Blocks is null) + { + return hasChanged; + } + + foreach (BlockItemData contentData in richTextValue.Blocks.ContentData) + { + if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue)) + { + hasChanged = true; + } + } + + foreach (BlockItemData settingsData in richTextValue.Blocks.SettingsData) + { + if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue)) + { + hasChanged = true; + } + } + + return hasChanged; + } +} From 9ad4a7eeba2472cff192860f7f3064fa6ba0097d Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 11 Nov 2025 11:50:23 +0000 Subject: [PATCH 137/155] Adds Clear Clipboard button & logic (#20757) * Adds new dictionary/localization item for the clipboard dialog clear all prompt * Removes the wrapping uui-box and moved inside the component itself * Adds Clear Clipboard button and logic * Adds uui-box from outer components consuimg this into this component * Adds a header to uui-box * Adds a conditional uui-button when we have items in clipboard * Adds confirm dialog/prompt to ask if user wants to clear all items * Adds in general_clipboard item to use in the UUI-box header * Removes extra space & moves the requestItems outside the for loop * Be a better citizen Make sure the promise for the modal is caught and we return out early if user explictiy cancels modal or presses ESC * Cleanup my noisy comments for a re-review --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .../src/assets/lang/en.ts | 2 + .../block-catalogue-modal.element.ts | 8 +-- .../clipboard-entry-picker-modal.element.ts | 10 ++- .../picker/clipboard-entry-picker.element.ts | 66 +++++++++++++++---- 4 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 6331f883356f..a04d3fe3cc3c 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -1017,6 +1017,7 @@ export default { if (new Date(date).getTime() < new Date(now).getTime()) return `${duration} ago`; return `in ${duration}`; }, + clipboard: 'Clipboard', }, colors: { black: 'Black', @@ -2508,6 +2509,7 @@ export default { labelForCopyToClipboard: 'Copy to clipboard', confirmDeleteHeadline: 'Delete from clipboard', confirmDeleteDescription: 'Are you sure you want to delete {0} from the clipboard?', + confirmClearDescription: 'Are you sure you want to clear the clipboard?', copySuccessHeadline: 'Copied to clipboard', }, propertyActions: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts index 08d3760117dd..f6ceed0051c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts @@ -194,11 +194,9 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< #renderClipboard() { return html` - - - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker-modal/clipboard-entry-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker-modal/clipboard-entry-picker-modal.element.ts index b3e273cf94e5..cfd5de0ea05c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker-modal/clipboard-entry-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker-modal/clipboard-entry-picker-modal.element.ts @@ -27,12 +27,10 @@ export class UmbClipboardEntryPickerModalElement extends UmbModalBaseElement< override render() { return html` - - - +
    diff --git a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts index fda340058b6c..46e9c393d73e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/clipboard/clipboard-entry/picker/clipboard-entry-picker.element.ts @@ -1,6 +1,7 @@ import { UmbClipboardCollectionRepository } from '../../collection/index.js'; import type { UmbClipboardEntryDetailModel } from '../types.js'; -import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import UmbClipboardEntryDetailRepository from '../detail/clipboard-entry-detail.repository.js'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbEntityContext } from '@umbraco-cms/backoffice/entity'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { @@ -10,6 +11,7 @@ import { import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; +import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; // TODO: make this into an extension point (Picker) with two kinds of pickers: tree-item-picker and collection-item-picker; @customElement('umb-clipboard-entry-picker') @@ -28,6 +30,8 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement { #entityContext = new UmbEntityContext(this); #actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; + #clipboardDetailRepository = new UmbClipboardEntryDetailRepository(this); + constructor() { super(); this.#entityContext.setEntityType('clipboard-entry'); @@ -117,17 +121,57 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement { } }; + async #clearClipboard() { + try { + await umbConfirmModal(this, { + headline: '#clipboard_labelForClearClipboard', + content: '#clipboard_confirmClearDescription', + color: 'danger', + confirmLabel: '#general_clear', + cancelLabel: '#general_cancel', + }); + } catch { + return; + } + + for (const item of this._items) { + const { error } = await this.#clipboardDetailRepository.delete(item.unique); + if (error) { + console.error(`Unable to delete clipboard item with unique ${item.unique}`, error); + } + } + + this.#requestItems(); + } + override render() { - return when( - this._items.length > 0, - () => - repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), - ), - () => html`

    There are no items in the clipboard.

    `, - ); + return html` + + + ${when( + this._items.length > 0, + () => html` + + + Clear Clipboard + + `, + () => nothing, + )} + + + ${when( + this._items.length > 0, + () => + repeat( + this._items, + (item) => item.unique, + (item) => this.#renderItem(item), + ), + () => html`

    There are no items in the clipboard.

    `, + )} +
    + `; } #renderItem(item: UmbClipboardEntryDetailModel) { From c9fc2f2a198b896fe9346eea5d0d8d8116889c1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:33:21 +0700 Subject: [PATCH 138/155] Bump playwright and @playwright/test in /tests/Umbraco.Tests.AcceptanceTest (#20579) * Bump playwright and @playwright/test Bumps [playwright](https://github.com/microsoft/playwright) to 1.56.1 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together. Updates `playwright` from 1.50.0 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.50.0...v1.56.1) Updates `@playwright/test` from 1.50.0 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.50.0...v1.56.1) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.56.1 dependency-type: indirect - dependency-name: "@playwright/test" dependency-version: 1.56.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] * Bumped version of test helper --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nhu Dinh --- .../package-lock.json | 40 +++++++++---------- .../Umbraco.Tests.AcceptanceTest/package.json | 4 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 6011fa4e8f65..86ba4e15b17d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,13 +8,13 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.41", - "@umbraco/playwright-testhelpers": "^17.0.3", + "@umbraco/playwright-testhelpers": "^17.0.6", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" }, "devDependencies": { - "@playwright/test": "1.50", + "@playwright/test": "1.56", "@types/node": "^20.9.0", "prompt": "^1.2.0", "tslib": "^2.4.0", @@ -32,13 +32,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", - "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.50.1" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -58,21 +58,21 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.41", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.41.tgz", - "integrity": "sha512-rCNUHCOpcuWIj7xUhk0lpcn4jzk9y82jHs9FSb7kxH716AnDyYvwuI+J0Ayd4hhWtXXqNCRqugCNYjG+rvzshQ==", + "version": "2.0.42", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.42.tgz", + "integrity": "sha512-5Zh/dSBGSKD9s0soemNnd5qT2h4gsKfTmQou/X34kqELSln333XMMfg+rbHfMleDwSBxh4dWAulntQFsfX0VtA==", "license": "MIT", "dependencies": { "camelize": "^1.0.1" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.3.tgz", - "integrity": "sha512-nlc7c7l/E264De+Z/niPfTT8yfRPauEmwbpX+N85PD30iM0mGLHd80InfM0mia19kL2njUm9ww3X8p7ZwBUM/g==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.6.tgz", + "integrity": "sha512-M0e5HJCqSTDxORFhebaNNGzBB4v6+77MerK6ctG1f+bU3JHfmbGZr4A4HDkD9eAeU7WGu5q7xoASdI0J1wqb1w==", "license": "MIT", "dependencies": { - "@umbraco/json-models-builders": "2.0.41", + "@umbraco/json-models-builders": "2.0.42", "node-fetch": "^2.6.7" } }, @@ -189,13 +189,13 @@ } }, "node_modules/playwright": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", - "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.50.1" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -208,9 +208,9 @@ } }, "node_modules/playwright-core": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", - "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 4697f58b5428..ca7c4b3f6f16 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -15,7 +15,7 @@ "testWindows": "npx playwright test DefaultConfig --grep-invert \"RelationType\"" }, "devDependencies": { - "@playwright/test": "1.50", + "@playwright/test": "1.56", "@types/node": "^20.9.0", "prompt": "^1.2.0", "tslib": "^2.4.0", @@ -23,7 +23,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.41", - "@umbraco/playwright-testhelpers": "^17.0.3", + "@umbraco/playwright-testhelpers": "^17.0.6", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" From c60cf90ffe5c2dd005b75a7302e56b750e3e39b0 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:18:53 +0100 Subject: [PATCH 139/155] E2E: QA added entity picker acceptance tests (#20776) * Added entity picker settings * Updated tests * Updated nightly pipeline * Updated tests * Bumped versions * Fixed indentation * Added comments * Cleaned up * Removed duplicate * Bumped version * Updated naming --- build/nightly-E2E-test-pipelines.yml | 17 +++ .../package-lock.json | 2 +- .../Umbraco.Tests.AcceptanceTest/package.json | 4 +- .../playwright.config.ts | 11 ++ .../picker-data-source/collection-api.js | 73 ++++++++++ .../picker-data-source/tree-api.js | 129 ++++++++++++++++++ .../picker-data-source/umbraco-package.json | 30 ++++ .../AdditionalSetup/appsettings.json | 58 ++++++++ .../Content/EntityPickerCollection.spec.ts | 127 +++++++++++++++++ .../Content/EntityPickerTree.spec.ts | 127 +++++++++++++++++ .../DataType/EntityPickerCollection.spec.ts | 33 +++++ .../DataType/EntityPickerTree.spec.ts | 33 +++++ .../EntityPickerCollection.spec.ts | 39 ++++++ .../RenderedContent/EntityPickerTree.spec.ts | 39 ++++++ 14 files changed, 719 insertions(+), 3 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/collection-api.js create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/tree-api.js create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/umbraco-package.json create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/appsettings.json create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/Content/EntityPickerCollection.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/Content/EntityPickerTree.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/DataType/EntityPickerCollection.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/DataType/EntityPickerTree.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/RenderedContent/EntityPickerCollection.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/RenderedContent/EntityPickerTree.spec.ts diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml index 8ed64d60303d..ba4e3d13e464 100644 --- a/build/nightly-E2E-test-pipelines.yml +++ b/build/nightly-E2E-test-pipelines.yml @@ -564,6 +564,23 @@ stages: CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient additionalEnvironmentVariables: false + # EntityDataPicker + WindowsEntityDataPicker: + vmImage: "windows-latest" + testFolder: "EntityDataPicker" + port: '' + testCommand: "npx playwright test --project=entityDataPicker" + CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True + CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient + additionalEnvironmentVariables: false + LinuxEntityDataPicker: + vmImage: "ubuntu-latest" + testFolder: "EntityDataPicker" + port: '' + testCommand: "npx playwright test --project=entityDataPicker" + CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True + CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient + additionalEnvironmentVariables: false pool: vmImage: $(vmImage) steps: diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 86ba4e15b17d..509985c5b118 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,7 +7,7 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^2.0.41", + "@umbraco/json-models-builders": "^2.0.42", "@umbraco/playwright-testhelpers": "^17.0.6", "camelize": "^1.0.0", "dotenv": "^16.3.1", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index ca7c4b3f6f16..7528914b7dc9 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -22,10 +22,10 @@ "typescript": "^4.8.3" }, "dependencies": { - "@umbraco/json-models-builders": "^2.0.41", + "@umbraco/json-models-builders": "^2.0.42", "@umbraco/playwright-testhelpers": "^17.0.6", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" } -} \ No newline at end of file +} diff --git a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts index 72776856bdeb..f31d53bcee3a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts @@ -65,6 +65,17 @@ export default defineConfig({ storageState: STORAGE_STATE } }, + { + name: 'entityDataPicker', + testMatch: 'EntityDataPicker/**/*.spec.ts', + dependencies: ['setup'], + use: { + ...devices['Desktop Chrome'], + // Use prepared auth state. + ignoreHTTPSErrors: true, + storageState: STORAGE_STATE + } + }, { name: 'deliveryApi', testMatch: 'DeliveryApi/**', diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/collection-api.js b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/collection-api.js new file mode 100644 index 000000000000..4a1f014e0da3 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/collection-api.js @@ -0,0 +1,73 @@ +import {UmbControllerBase} from "@umbraco-cms/backoffice/class-api"; + +export class ExampleCustomPickerCollectionPropertyEditorDataSource extends UmbControllerBase { + collectionPickableFilter = (item) => item.isPickable; + + async requestCollection(args) { + const data = { + items: customItems, + total: customItems.length, + }; + + return {data}; + } + + async requestItems(uniques) { + const items = customItems.filter((x) => uniques.includes(x.unique)); + return {data: items}; + } + + async search(args) { + const items = customItems.filter((item) => + item.name?.toLowerCase().includes(args.query.toLowerCase()) + ); + const total = items.length; + + const data = { + items, + total, + }; + + return {data}; + } +} + +export {ExampleCustomPickerCollectionPropertyEditorDataSource as api}; + +const customItems = [ + { + unique: "1", + entityType: "example", + name: "Example 1", + icon: "icon-shape-triangle", + isPickable: true, + }, + { + unique: "2", + entityType: "example", + name: "Example 2", + icon: "icon-shape-triangle", + isPickable: true, + }, + { + unique: "3", + entityType: "example", + name: "Example 3", + icon: "icon-shape-triangle", + isPickable: true, + }, + { + unique: "4", + entityType: "example", + name: "Example 4", + icon: "icon-shape-triangle", + isPickable: false, + }, + { + unique: "5", + entityType: "example", + name: "Example 5", + icon: "icon-shape-triangle", + isPickable: true, + }, +]; diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/tree-api.js b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/tree-api.js new file mode 100644 index 000000000000..3fc7a208d9b5 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/tree-api.js @@ -0,0 +1,129 @@ +import {UmbControllerBase} from "@umbraco-cms/backoffice/class-api"; + +export class MyPickerDataSource extends UmbControllerBase { + treePickableFilter = (treeItem) => + !!treeItem.unique && treeItem.entityType === "example"; + + searchPickableFilter = (searchItem) => + !!searchItem.unique && searchItem.entityType === "example"; + + async requestTreeRoot() { + const root = { + unique: null, + name: "Examples", + icon: "icon-folder", + hasChildren: true, + entityType: "example-root", + isFolder: true, + }; + + return {data: root}; + } + + async requestTreeRootItems() { + const rootItems = customItems.filter((item) => item.parent.unique === null); + + const data = { + items: rootItems, + total: rootItems.length, + }; + + return {data}; + } + + async requestTreeItemsOf(args) { + const items = customItems.filter( + (item) => + item.parent.entityType === args.parent.entityType && + item.parent.unique === args.parent.unique + ); + + const data = { + items: items, + total: items.length, + }; + + return {data}; + } + + async requestTreeItemAncestors() { + // TODO: implement when needed + return {data: []}; + } + + async requestItems(uniques) { + const items = customItems.filter((x) => uniques.includes(x.unique)); + return {data: items}; + } + + async search(args) { + const result = customItems.filter((item) => + item.name.toLowerCase().includes(args.query.toLowerCase()) + ); + + const data = { + items: result, + total: result.length, + }; + + return {data}; + } +} + +export {MyPickerDataSource as api}; + +const customItems = [ + { + unique: "1", + entityType: "example", + name: "Example 1", + icon: "icon-shape-triangle", + parent: {unique: null, entityType: "example-root"}, + isFolder: false, + hasChildren: false, + }, + { + unique: "2", + entityType: "example", + name: "Example 2", + icon: "icon-shape-triangle", + parent: {unique: null, entityType: "example-root"}, + isFolder: false, + hasChildren: false, + }, + { + unique: "3", + entityType: "example", + name: "Example 3", + icon: "icon-shape-triangle", + parent: {unique: null, entityType: "example-root"}, + isFolder: false, + hasChildren: false, + }, + { + unique: "4", + entityType: "example", + name: "Example 4", + icon: "icon-shape-triangle", + parent: {unique: "6", entityType: "example-folder"}, + isFolder: false, + hasChildren: false, + }, + { + unique: "5", + entityType: "example", + name: "Example 5", + icon: "icon-shape-triangle", + parent: {unique: "6", entityType: "example-folder"}, + isFolder: false, + hasChildren: false, + }, + { + unique: "6", + entityType: "example-folder", + name: "Example Folder 1", + parent: {unique: null, entityType: "example-root"}, + isFolder: true, + hasChildren: true, + }, +]; diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/umbraco-package.json b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/umbraco-package.json new file mode 100644 index 000000000000..08cbc8163dd6 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/App_Plugins/picker-data-source/umbraco-package.json @@ -0,0 +1,30 @@ +{ + "name": "My Picker Data Source", + "alias": "My.PickerDataSource", + "extensions": [ + { + "type": "propertyEditorDataSource", + "dataSourceType": "Umb.DataSourceType.Picker", + "alias": "My.PickerDataSource.Tree", + "name": "My Picker Tree Data Source", + "api": "/App_Plugins/picker-data-source/tree-api.js", + "meta": { + "icon": "icon-database", + "label": "My Picker Tree Data Source", + "description": "Some description goes here" + } + }, + { + "type": "propertyEditorDataSource", + "dataSourceType": "Umb.DataSourceType.Picker", + "alias": "My.PickerDataSource.Collection", + "name": "My Picker Collection Data Source", + "api": "/App_Plugins/picker-data-source/collection-api.js", + "meta": { + "icon": "icon-database", + "label": "My Picker Collection Data Source", + "description": "Some description goes here" + } + } + ] +} diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/appsettings.json b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/appsettings.json new file mode 100644 index 000000000000..49d90bb5936e --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/AdditionalSetup/appsettings.json @@ -0,0 +1,58 @@ +{ + "$schema": "appsettings-schema.json", + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Async", + "Args": { + "Configure": [ + { + "Name": "Console" + } + ] + } + } + ] + }, + "Umbraco": { + "CMS": { + "Unattended": { + "InstallUnattended": true, + "UnattendedUserName": "Playwright Test", + "UnattendedUserEmail": "playwright@umbraco.com", + "UnattendedUserPassword": "UmbracoAcceptance123!" + }, + "Content": { + "ContentVersionCleanupPolicy": { + "EnableCleanup": false + } + }, + "Global": { + "DisableElectionForSingleServer": true, + "InstallMissingDatabase": true, + "Id": "00000000-0000-0000-0000-000000000042", + "VersionCheckPeriod": 0, + "UseHttps": true + }, + "HealthChecks": { + "Notification": { + "Enabled": false + } + }, + "KeepAlive": { + "DisableKeepAliveTask": true + }, + "WebRouting": { + "UmbracoApplicationUrl": "https://localhost:44331/" + } + } + } +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/Content/EntityPickerCollection.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/Content/EntityPickerCollection.spec.ts new file mode 100644 index 000000000000..6e03d37ab225 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/Content/EntityPickerCollection.spec.ts @@ -0,0 +1,127 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'EntityPickerWithCollection'; +const collectionDataSourceAlias = 'My.PickerDataSource.Collection'; + +test.beforeEach(async ({umbracoUi}) => { + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(dataTypeName); +}); + +test('can create empty content with an entity picker using the collection data source', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataType(dataTypeName, collectionDataSourceAlias); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateActionMenuOption(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.waitForContentToBeCreated(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can create content with an entity picker using the collection data source that has an item', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataType(dataTypeName, collectionDataSourceAlias); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseCollectionMenuItemWithName('Example 1'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.waitForContentToBeCreated(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value.ids[0]).toEqual('1'); +}); + +test('can create content with an entity picker using the collection data source that has multiple items', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataType(dataTypeName, collectionDataSourceAlias); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseCollectionMenuItemWithName('Example 1'); + await umbracoUi.content.chooseCollectionMenuItemWithName('Example 3'); + await umbracoUi.content.chooseCollectionMenuItemWithName('Example 5'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.waitForContentToBeCreated(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value.ids[0]).toEqual('1'); + expect(contentData.values[0].value.ids[1]).toEqual('3'); + expect(contentData.values[0].value.ids[2]).toEqual('5'); +}); + +test('can not create content with an entity picker using the collection data source that has more items than max amount', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataTypeWithMinAndMaxValues(dataTypeName, collectionDataSourceAlias, 0, 2); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseCollectionMenuItemWithName('Example 1'); + await umbracoUi.content.isChooseButtonVisible(true); + await umbracoUi.content.chooseCollectionMenuItemWithName('Example 3'); + + // Assert + // The choose button should be disabled when the max amount is reached + await umbracoUi.content.isChooseButtonVisible(false); +}); + +test('can not create content with an entity picker using the collection data source that has less items than min amount', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataTypeWithMinAndMaxValues(dataTypeName, collectionDataSourceAlias, 2, 5); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseCollectionMenuItemWithName('Example 1'); + await umbracoUi.content.isTextWithExactNameVisible('This field need more items'); + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); + await umbracoUi.content.chooseCollectionMenuItemWithName('Example 3'); + + // Assert + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.waitForContentToBeCreated(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/Content/EntityPickerTree.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/Content/EntityPickerTree.spec.ts new file mode 100644 index 000000000000..494974c511fa --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/Content/EntityPickerTree.spec.ts @@ -0,0 +1,127 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'EntityPickerWithTree'; +const treeDataSourceAlias = 'My.PickerDataSource.Tree'; + +test.beforeEach(async ({umbracoUi}) => { + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.dataType.ensureNameNotExists(dataTypeName); +}); + +test('can create empty content with an entity picker using the tree data source', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataType(dataTypeName, treeDataSourceAlias); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuAtRoot(); + await umbracoUi.content.clickCreateActionMenuOption(); + await umbracoUi.content.chooseDocumentType(documentTypeName); + await umbracoUi.content.enterContentName(contentName); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.waitForContentToBeCreated(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values).toEqual([]); +}); + +test('can create content with an entity picker using the tree data source that has an item', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataType(dataTypeName, treeDataSourceAlias); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseTreeMenuItemWithName('Example 1'); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.waitForContentToBeCreated(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value.ids[0]).toEqual('1'); +}); + +test('can create content with an entity picker using the tree data source that has multiple items', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Draft'; + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataType(dataTypeName, treeDataSourceAlias); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseTreeMenuItemWithName('Example 1'); + await umbracoUi.content.chooseTreeMenuItemWithName('Example 3'); + await umbracoUi.content.chooseTreeMenuItemWithName('Example 5', ['Example Folder 1']); + await umbracoUi.content.clickSaveButton(); + + // Assert + await umbracoUi.content.waitForContentToBeCreated(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); + expect(contentData.values[0].value.ids[0]).toEqual('1'); + expect(contentData.values[0].value.ids[1]).toEqual('3'); + expect(contentData.values[0].value.ids[2]).toEqual('5'); +}); + +test('can not create content with an entity picker using the tree data source that has more items than max amount', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataTypeWithMinAndMaxValues(dataTypeName, treeDataSourceAlias, 0, 2); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseTreeMenuItemWithName('Example 1'); + await umbracoUi.content.isChooseButtonVisible(true); + await umbracoUi.content.chooseTreeMenuItemWithName('Example 5', ['Example Folder 1']); + + // Assert + // The choose button should be disabled when the max amount is reached + await umbracoUi.content.isChooseButtonVisible(false); +}); + +test('can not create content with an entity picker using the tree data source that has less items than min amount', async ({umbracoApi, umbracoUi}) => { + // Arrange + const expectedState = 'Published'; + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataTypeWithMinAndMaxValues(dataTypeName, treeDataSourceAlias, 2, 5); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.goToContentWithName(contentName); + await umbracoUi.content.chooseTreeMenuItemWithName('Example 1'); + await umbracoUi.content.isTextWithExactNameVisible('This field need more items'); + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); + await umbracoUi.content.chooseTreeMenuItemWithName('Example 5', ['Example Folder 1']); + + // Assert + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.waitForContentToBeCreated(); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy(); + const contentData = await umbracoApi.document.getByName(contentName); + expect(contentData.variants[0].state).toBe(expectedState); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/DataType/EntityPickerCollection.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/DataType/EntityPickerCollection.spec.ts new file mode 100644 index 000000000000..1096ebdb4f7a --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/DataType/EntityPickerCollection.spec.ts @@ -0,0 +1,33 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'EntityPickerWithCollection'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSection(ConstantHelper.sections.settings); + await umbracoApi.dataType.ensureNameNotExists(dataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(dataTypeName); +}); + +test('can create an entity picker data type with the collection data source', async ({umbracoApi, umbracoUi}) => { + // Act + await umbracoUi.dataType.clickActionsMenuForName('Data Types'); + await umbracoUi.dataType.clickCreateActionMenuOption(); + await umbracoUi.dataType.clickDataTypeButton(); + await umbracoUi.dataType.enterDataTypeName(dataTypeName); + await umbracoUi.dataType.clickSelectAPropertyEditorButton(); + await umbracoUi.dataType.selectAPropertyEditor('Entity Data Picker'); + await umbracoUi.dataType.clickChooseDataSourceButton(); + await umbracoUi.dataType.clickButtonWithName('My Picker Collection Data Source'); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.waitForDataTypeToBeCreated(); + await umbracoUi.dataType.isDataTypeTreeItemVisible(dataTypeName); + expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/DataType/EntityPickerTree.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/DataType/EntityPickerTree.spec.ts new file mode 100644 index 000000000000..90c04b5e9505 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/DataType/EntityPickerTree.spec.ts @@ -0,0 +1,33 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const dataTypeName = 'EntityPickerWithTree'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSection(ConstantHelper.sections.settings); + await umbracoApi.dataType.ensureNameNotExists(dataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(dataTypeName); +}); + +test('can create an entity picker data type with tree data source', async ({umbracoApi, umbracoUi}) => { + // Act + await umbracoUi.dataType.clickActionsMenuForName('Data Types'); + await umbracoUi.dataType.clickCreateActionMenuOption(); + await umbracoUi.dataType.clickDataTypeButton(); + await umbracoUi.dataType.enterDataTypeName(dataTypeName); + await umbracoUi.dataType.clickSelectAPropertyEditorButton(); + await umbracoUi.dataType.selectAPropertyEditor('Entity Data Picker'); + await umbracoUi.dataType.clickChooseDataSourceButton(); + await umbracoUi.dataType.clickButtonWithName('My Picker Tree Data Source'); + await umbracoUi.dataType.clickChooseModalButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.waitForDataTypeToBeCreated(); + await umbracoUi.dataType.isDataTypeTreeItemVisible(dataTypeName); + expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/RenderedContent/EntityPickerCollection.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/RenderedContent/EntityPickerCollection.spec.ts new file mode 100644 index 000000000000..824d22b1d2ab --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/RenderedContent/EntityPickerCollection.spec.ts @@ -0,0 +1,39 @@ +import {AliasHelper, test} from '@umbraco/playwright-testhelpers'; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const templateName = 'EntityPickerCollectionTemplate'; +const dataTypeName = 'EntityPickerWithCollection'; +const propertyName = 'TestProperty'; +const collectionDataSourceAlias = 'My.PickerDataSource.Collection'; + +// Ids for Example 4 and Example 2 +const items = {ids: ['4', '2']}; + +test.beforeEach(async ({umbracoUi}) => { + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.template.ensureNameNotExists(templateName); + await umbracoApi.dataType.ensureNameNotExists(dataTypeName); +}); + +test('can render content with an entity picker using the collection data source', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataType(dataTypeName, collectionDataSourceAlias); + const templateId = await umbracoApi.template.createTemplateWithEntityDataPickerValue(templateName, propertyName); + const contentKey = await umbracoApi.document.createPublishedDocumentWithValue(contentName, items, dataTypeId, templateId, propertyName, documentTypeName); + const contentURL = await umbracoApi.document.getDocumentUrl(contentKey); + + // Act + await umbracoUi.contentRender.navigateToRenderedContentPage(contentURL); + + // Assert + await umbracoUi.contentRender.doesDataSourceRenderValueHaveText(collectionDataSourceAlias); + for (const value of items.ids) { + await umbracoUi.contentRender.doesContentRenderValueContainText(value); + } +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/RenderedContent/EntityPickerTree.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/RenderedContent/EntityPickerTree.spec.ts new file mode 100644 index 000000000000..192a0f8dd3e9 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/EntityDataPicker/RenderedContent/EntityPickerTree.spec.ts @@ -0,0 +1,39 @@ +import {AliasHelper, test} from '@umbraco/playwright-testhelpers'; + +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const templateName = 'EntityPickerTreeTemplate'; +const dataTypeName = 'EntityPickerWithTree'; +const propertyName = 'TestProperty'; +const treeDataSourceAlias = 'My.PickerDataSource.Tree'; + +// Ids for Example 4 and Example 2 +const items = {ids: ['4', '2']}; + +test.beforeEach(async ({umbracoUi}) => { + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.template.ensureNameNotExists(templateName); + await umbracoApi.dataType.ensureNameNotExists(dataTypeName); +}); + +test('can render content with an entity picker using the tree data source', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeId = await umbracoApi.dataType.createEntityDataPickerDataType(dataTypeName, treeDataSourceAlias); + const templateId = await umbracoApi.template.createTemplateWithEntityDataPickerValue(templateName, propertyName); + const contentKey = await umbracoApi.document.createPublishedDocumentWithValue(contentName, items, dataTypeId, templateId, propertyName, documentTypeName); + const contentURL = await umbracoApi.document.getDocumentUrl(contentKey); + + // Act + await umbracoUi.contentRender.navigateToRenderedContentPage(contentURL); + + // Assert + await umbracoUi.contentRender.doesDataSourceRenderValueHaveText(treeDataSourceAlias); + for (const value of items.ids) { + await umbracoUi.contentRender.doesContentRenderValueContainText(value); + } +}); From ca15aadf0ea7492ac4d18a2324f81ec233018a26 Mon Sep 17 00:00:00 2001 From: Justin Neville <67802060+justin-nevitech@users.noreply.github.com> Date: Thu, 13 Nov 2025 00:21:26 +0000 Subject: [PATCH 140/155] =?UTF-8?q?Fix=20for=20partial=20view=20caches=20n?= =?UTF-8?q?ot=20being=20cleared=20when=20content=20is=20publish=E2=80=A6?= =?UTF-8?q?=20(#20794)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix for partial view caches not being cleared when content is published/unpublished * Update src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Change logic for clearing partial view cache * Changed logic to only clear partial cache when content is published/unpublished or trashed --------- Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Implement/ContentCacheRefresher.cs | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index 7505781ad95a..b1307ef1e8bb 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -151,6 +149,12 @@ public override void Refresh(JsonPayload[] payloads) } + // Clear partial view cache when published content changes + if (ShouldClearPartialViewCache(payloads)) + { + AppCaches.ClearPartialViewCache(); + } + if (idsRemoved.Count > 0) { var assignedDomains = _domainService.GetAll(true) @@ -175,6 +179,28 @@ public override void Refresh(JsonPayload[] payloads) base.Refresh(payloads); } + private static bool ShouldClearPartialViewCache(JsonPayload[] payloads) + { + return payloads.Any(x => + { + // Check for relelvant change type + var isRelevantChangeType = x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll) || + x.ChangeTypes.HasType(TreeChangeTypes.Remove) || + x.ChangeTypes.HasType(TreeChangeTypes.RefreshNode) || + x.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch); + + // Check for published/unpublished changes + var hasChanges = x.PublishedCultures?.Length > 0 || + x.UnpublishedCultures?.Length > 0; + + // There's no other way to detect trashed content as the change type is only Remove when deleted permanently + var isTrashed = x.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch) && x.PublishedCultures is null && x.UnpublishedCultures is null; + + // Skip blueprints and only clear the partial cache for removals or refreshes with changes + return x.Blueprint == false && (isTrashed || (isRelevantChangeType && hasChanges)); + }); + } + private void HandleMemoryCache(JsonPayload payload) { Guid key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result; @@ -365,7 +391,7 @@ private async Task HandlePublishedAsync(JsonPayload payload, CancellationToken c } private void HandleRouting(JsonPayload payload) { - if(payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) + if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result; @@ -374,24 +400,24 @@ private void HandleRouting(JsonPayload payload) { _documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeys).GetAwaiter().GetResult(); } - else if(_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out var descendantsOrSelfKeysInBin)) + else if (_documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeysInBin(key, out var descendantsOrSelfKeysInBin)) { _documentUrlService.DeleteUrlsFromCacheAsync(descendantsOrSelfKeysInBin).GetAwaiter().GetResult(); } } - if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) + if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { _documentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult(); //TODO make async } - if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode)) + if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode)) { var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result; _documentUrlService.CreateOrUpdateUrlSegmentsAsync(key).GetAwaiter().GetResult(); } - if(payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) + if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { var key = payload.Key ?? _idKeyMap.GetKeyForId(payload.Id, UmbracoObjectTypes.Document).Result; _documentUrlService.CreateOrUpdateUrlSegmentsWithDescendantsAsync(key).GetAwaiter().GetResult(); From 139b528bda622d76130e2c8c5070186dbafd8d75 Mon Sep 17 00:00:00 2001 From: NguyenThuyLan <116753400+NguyenThuyLan@users.noreply.github.com> Date: Thu, 13 Nov 2025 11:10:51 +0700 Subject: [PATCH 141/155] Database migrations: Support `DateOnly` and `TimeOnly` in syntax providers (#20784) * sql column type map include dateonly and timeonly * Split Mapper and add check null value * Minor code tidy resolving a few warnings. * add spaces * clean code --------- Co-authored-by: Lan Nguyen Thuy Co-authored-by: Andy Butland --- .../SqlitePocoDateAndTimeOnlyMapper.cs | 64 +++++++++++++++++ .../Mappers/SqlitePocoDecimalMapper.cs | 26 +++++++ .../Mappers/SqlitePocoGuidMapper.cs | 29 ++------ .../Services/SqliteSpecificMapperFactory.cs | 2 +- .../Mapping/UmbracoDefaultMapper.cs | 68 ++++++++++++++++--- .../SqlSyntax/SqlSyntaxProviderBase.cs | 8 +++ .../Testing/SqliteTestDatabase.cs | 2 + 7 files changed, 163 insertions(+), 36 deletions(-) create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDateAndTimeOnlyMapper.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDecimalMapper.cs diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDateAndTimeOnlyMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDateAndTimeOnlyMapper.cs new file mode 100644 index 000000000000..4e3ec6a411f1 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDateAndTimeOnlyMapper.cs @@ -0,0 +1,64 @@ +using NPoco; + +namespace Umbraco.Cms.Persistence.Sqlite.Mappers; + +/// +/// Provides a custom POCO mapper for handling date and time only values when working with SQLite databases. +/// +public class SqlitePocoDateAndTimeOnlyMapper : DefaultMapper +{ + /// + public override Func GetFromDbConverter(Type destType, Type sourceType) + { + if (IsDateOnlyType(destType)) + { + return value => ConvertToDateOnly(value, IsNullableType(destType)); + } + + if (IsTimeOnlyType(destType)) + { + return value => ConvertToTimeOnly(value, IsNullableType(destType)); + } + + return base.GetFromDbConverter(destType, sourceType); + } + + private static bool IsDateOnlyType(Type type) => + type == typeof(DateOnly) || type == typeof(DateOnly?); + + private static bool IsTimeOnlyType(Type type) => + type == typeof(TimeOnly) || type == typeof(TimeOnly?); + + private static bool IsNullableType(Type type) => + Nullable.GetUnderlyingType(type) != null; + + private static object? ConvertToDateOnly(object? value, bool isNullable) + { + if (value is null) + { + return isNullable ? null : default(DateOnly); + } + + if (value is DateTime dt) + { + return DateOnly.FromDateTime(dt); + } + + return DateOnly.Parse(value.ToString()!); + } + + private static object? ConvertToTimeOnly(object? value, bool isNullable) + { + if (value is null) + { + return isNullable ? null : default(TimeOnly); + } + + if (value is DateTime dt) + { + return TimeOnly.FromDateTime(dt); + } + + return TimeOnly.Parse(value.ToString()!); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDecimalMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDecimalMapper.cs new file mode 100644 index 000000000000..567b1cbcb8f9 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoDecimalMapper.cs @@ -0,0 +1,26 @@ +using System.Globalization; +using NPoco; + +namespace Umbraco.Cms.Persistence.Sqlite.Mappers; + +/// +/// Provides a custom POCO mapper for handling decimal values when working with SQLite databases. +/// +public class SqlitePocoDecimalMapper : DefaultMapper +{ + /// + public override Func GetFromDbConverter(Type destType, Type sourceType) + { + if (destType == typeof(decimal)) + { + return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture); + } + + if (destType == typeof(decimal?)) + { + return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture); + } + + return base.GetFromDbConverter(destType, sourceType); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs index ab62b4b1d1c6..ffb96a6b2b0e 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs @@ -1,19 +1,18 @@ -using System.Globalization; using NPoco; namespace Umbraco.Cms.Persistence.Sqlite.Mappers; +/// +/// Provides a custom POCO mapper for handling GUID values when working with SQLite databases. +/// public class SqlitePocoGuidMapper : DefaultMapper { + /// public override Func GetFromDbConverter(Type destType, Type sourceType) { if (destType == typeof(Guid)) { - return value => - { - var result = Guid.Parse($"{value}"); - return result; - }; + return value => Guid.Parse($"{value}"); } if (destType == typeof(Guid?)) @@ -29,24 +28,6 @@ public class SqlitePocoGuidMapper : DefaultMapper }; } - if (destType == typeof(decimal)) - { - return value => - { - var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture); - return result; - }; - } - - if (destType == typeof(decimal?)) - { - return value => - { - var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture); - return result; - }; - } - return base.GetFromDbConverter(destType, sourceType); } } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs index 66f542712a3c..9c577d6329dd 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs @@ -12,5 +12,5 @@ public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory public string ProviderName => Constants.ProviderName; /// - public NPocoMapperCollection Mappers => new(() => new[] { new SqlitePocoGuidMapper() }); + public NPocoMapperCollection Mappers => new(() => [new SqlitePocoGuidMapper(), new SqlitePocoDecimalMapper(), new SqlitePocoDateAndTimeOnlyMapper()]); } diff --git a/src/Umbraco.Infrastructure/Mapping/UmbracoDefaultMapper.cs b/src/Umbraco.Infrastructure/Mapping/UmbracoDefaultMapper.cs index 986bb19d39d8..0afb677ee9ab 100644 --- a/src/Umbraco.Infrastructure/Mapping/UmbracoDefaultMapper.cs +++ b/src/Umbraco.Infrastructure/Mapping/UmbracoDefaultMapper.cs @@ -1,30 +1,76 @@ -using System.Globalization; +using System.Globalization; using NPoco; namespace Umbraco.Cms.Core.Mapping; +/// +/// Provides default type conversion logic for mapping Umbraco database values to .NET types, extending the base mapping +/// behavior with support for additional types such as decimal, DateOnly, and TimeOnly. +/// public class UmbracoDefaultMapper : DefaultMapper { + /// public override Func GetFromDbConverter(Type destType, Type sourceType) { if (destType == typeof(decimal)) { - return value => - { - var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture); - return result; - }; + return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture); } if (destType == typeof(decimal?)) { - return value => - { - var result = Convert.ToDecimal(value, CultureInfo.InvariantCulture); - return result; - }; + return value => Convert.ToDecimal(value, CultureInfo.InvariantCulture); + } + + if(IsDateOnlyType(destType)) + { + return value => ConvertToDateOnly(value, IsNullableType(destType)); + } + + if (IsTimeOnlyType(destType)) + { + return value => ConvertToTimeOnly(value, IsNullableType(destType)); } return base.GetFromDbConverter(destType, sourceType); } + + private static bool IsDateOnlyType(Type type) => + type == typeof(DateOnly) || type == typeof(DateOnly?); + + private static bool IsTimeOnlyType(Type type) => + type == typeof(TimeOnly) || type == typeof(TimeOnly?); + + private static bool IsNullableType(Type type) => + Nullable.GetUnderlyingType(type) != null; + + private static object? ConvertToDateOnly(object? value, bool isNullable) + { + if (value is null) + { + return isNullable ? null : default(DateOnly); + } + + if (value is DateTime dt) + { + return DateOnly.FromDateTime(dt); + } + + return DateOnly.Parse(value.ToString()!); + } + + private static object? ConvertToTimeOnly(object? value, bool isNullable) + { + if (value is null) + { + return isNullable ? null : default(TimeOnly); + } + + if (value is DateTime dt) + { + return TimeOnly.FromDateTime(dt); + } + + return TimeOnly.Parse(value.ToString()!); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 9cefd32b5bef..82bbf26f810e 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -100,6 +100,10 @@ protected SqlSyntaxProviderBase() public string TimeColumnDefinition { get; protected set; } = "DATETIME"; + public string DateOnlyColumnDefinition { get; protected set; } = "DATE"; + + public string TimeOnlyColumnDefinition { get; protected set; } = "TIME"; + protected IList> ClauseOrder { get; } protected DbTypes DbTypeMap => _dbTypes.Value; @@ -531,6 +535,10 @@ private DbTypes InitColumnTypeMap() dbTypeMap.Set(DbType.Time, TimeColumnDefinition); dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + dbTypeMap.Set(DbType.Date, DateOnlyColumnDefinition); + dbTypeMap.Set(DbType.Date, DateOnlyColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeOnlyColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeOnlyColumnDefinition); dbTypeMap.Set(DbType.Byte, IntColumnDefinition); dbTypeMap.Set(DbType.Byte, IntColumnDefinition); diff --git a/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs index a20b00ebe598..73b9c444a7ae 100644 --- a/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs @@ -96,6 +96,8 @@ protected override void RebuildSchema(IDbCommand command, TestDbMeta meta) database.Mappers.Add(new NullableDateMapper()); database.Mappers.Add(new SqlitePocoGuidMapper()); + database.Mappers.Add(new SqlitePocoDecimalMapper()); + database.Mappers.Add(new SqlitePocoDateAndTimeOnlyMapper()); foreach (var dbCommand in _cachedDatabaseInitCommands) { From c2952717577b450ed5391e909684609d37045b7d Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 13 Nov 2025 06:39:10 +0100 Subject: [PATCH 142/155] Bumped version to 16.4.0-rc2. --- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index afeacbe636ee..b076052b37d2 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.4.0-rc", + "version": "16.4.0-rc2", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index d8d415334bcc..cf14b3a81a6f 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": "16.4.0-rc", + "version": "16.4.0-rc2", "assemblyVersion": { "precision": "build" }, From 49ba89c22a7d51784cff43a13a3684a47dc47932 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 13 Nov 2025 08:19:42 +0100 Subject: [PATCH 143/155] Move access/refresh tokens to secure cookies (#20779) * feat: adds the `credentials: include` header to all manual requests * feat: adds `credentials: include` as a configurable option to xhr requests (and sets it by default to true) * feat: configures the auto-generated fetch client from hey-api to include credentials by default * Add OpenIddict handler to hide tokens from the back-office client * Make back-office token redaction optional (default false) * Clear back-office token cookies on logout * Add configuration for backoffice cookie settings * Make cookies forcefully secure + move cookie handler enabling to the BackOfficeTokenCookieSettings * Use the "__Host-" prefix for cookie names * docs: adds documentation on cookie settings * build: sets up launch profile for vscode with new cookie recommended settings * docs: adds extra note around SameSite settings * docs: adds extra note around SameSite settings * Respect sites that do not use HTTPS * Explicitly invalidate potentially valid, old refresh tokens that should no longer be used * Removed obsolete const --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .github/BUILD.md | 13 +- .github/copilot-instructions.md | 6 +- .vscode/launch.json | 5 +- .../HideBackOfficeTokensHandler.cs | 187 ++++++++++++++++++ .../UmbracoBuilderAuthExtensions.cs | 38 ++++ .../Models/BackOfficeTokenCookieSettings.cs | 31 +++ src/Umbraco.Core/Constants-Configuration.cs | 1 + .../UmbracoBuilder.Configuration.cs | 3 +- src/Umbraco.Web.UI.Client/.github/README.md | 12 +- .../src/external/openid/src/xhr.ts | 1 + .../src/packages/core/auth/auth-flow.ts | 2 + .../src/packages/core/http-client/index.ts | 15 +- .../try-execute/tryXhrRequest.function.ts | 1 + .../src/packages/core/resources/types.ts | 1 + .../document-permission.server.data.ts | 1 + 15 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs create mode 100644 src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs diff --git a/.github/BUILD.md b/.github/BUILD.md index 2e081548fee4..424a491ccecd 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -37,7 +37,7 @@ In order to work with the Umbraco source code locally, first make sure you have ### Familiarizing yourself with the code -Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes. +Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes. There are two web projects in the solution with client-side assets based on TypeScript, `Umbraco.Web.UI.Client` and `Umbraco.Web.UI.Login`. @@ -73,13 +73,20 @@ Just be careful not to include this change in your PR. Conversely, if you are working on front-end only, you want to build the back-end once and then run it. Before you do so, update the configuration in `appSettings.json` to add the following under `Umbraco:Cms:Security`: -``` +```json "BackOfficeHost": "http://localhost:5173", "AuthorizeCallbackPathName": "/oauth_complete", "AuthorizeCallbackLogoutPathName": "/logout", -"AuthorizeCallbackErrorPathName": "/error" +"AuthorizeCallbackErrorPathName": "/error", +"BackOfficeTokenCookie": { + "Enabled": true, + "SameSite": "None" +} ``` +> [!NOTE] +> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately. + Then run Umbraco from the command line. ``` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ed34279ab99b..858984a9d38a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -110,7 +110,11 @@ Use this for frontend-only development with hot module reloading: "BackOfficeHost": "http://localhost:5173", "AuthorizeCallbackPathName": "/oauth_complete", "AuthorizeCallbackLogoutPathName": "/logout", - "AuthorizeCallbackErrorPathName": "/error" + "AuthorizeCallbackErrorPathName": "/error", + "BackOfficeTokenCookie": { + "Enabled": true, + "SameSite": "None" + } ``` 2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build` 3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server` diff --git a/.vscode/launch.json b/.vscode/launch.json index f4d47c3dab86..c56f06dc2f8d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -105,7 +105,10 @@ "UMBRACO__CMS__SECURITY__BACKOFFICEHOST": "http://localhost:5173", "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKPATHNAME": "/oauth_complete", "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout", - "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error" + "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error", + "UMBRACO__CMS__SECURITY__KEEPUSERLOGGEDIN": "true", + "UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__ENABLED": "true", + "UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Umbraco.Web.UI/Views" diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs new file mode 100644 index 000000000000..8d1dbd040ed4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs @@ -0,0 +1,187 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using OpenIddict.Server; +using OpenIddict.Validation; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Common.DependencyInjection; + +internal sealed class HideBackOfficeTokensHandler + : IOpenIddictServerHandler, + IOpenIddictServerHandler, + IOpenIddictValidationHandler, + INotificationHandler +{ + private const string RedactedTokenValue = "[redacted]"; + private const string AccessTokenCookieKey = "__Host-umbAccessToken"; + private const string RefreshTokenCookieKey = "__Host-umbRefreshToken"; + + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly BackOfficeTokenCookieSettings _backOfficeTokenCookieSettings; + private readonly GlobalSettings _globalSettings; + + public HideBackOfficeTokensHandler( + IHttpContextAccessor httpContextAccessor, + IDataProtectionProvider dataProtectionProvider, + IOptions backOfficeTokenCookieSettings, + IOptions globalSettings) + { + _httpContextAccessor = httpContextAccessor; + _dataProtectionProvider = dataProtectionProvider; + _backOfficeTokenCookieSettings = backOfficeTokenCookieSettings.Value; + _globalSettings = globalSettings.Value; + } + + /// + /// This is invoked when tokens (access and refresh tokens) are issued to a client. For the back-office client, + /// we will intercept the response, write the tokens from the response into HTTP-only cookies, and redact the + /// tokens from the response, so they are not exposed to the client. + /// + public ValueTask HandleAsync(OpenIddictServerEvents.ApplyTokenResponseContext context) + { + if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice) + { + // Only ever handle the back-office client. + return ValueTask.CompletedTask; + } + + HttpContext httpContext = GetHttpContext(); + + if (context.Response.AccessToken is not null) + { + SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken); + context.Response.AccessToken = RedactedTokenValue; + } + + if (context.Response.RefreshToken is not null) + { + SetCookie(httpContext, RefreshTokenCookieKey, context.Response.RefreshToken); + context.Response.RefreshToken = RedactedTokenValue; + } + + return ValueTask.CompletedTask; + } + + /// + /// This is invoked when requesting new tokens. + /// + public ValueTask HandleAsync(OpenIddictServerEvents.ExtractTokenRequestContext context) + { + if (context.Request?.ClientId != Constants.OAuthClientIds.BackOffice) + { + // Only ever handle the back-office client. + return ValueTask.CompletedTask; + } + + // For the back-office client, this only happens when a refresh token is being exchanged for a new access token. + if (context.Request.RefreshToken == RedactedTokenValue + && TryGetCookie(RefreshTokenCookieKey, out var refreshToken)) + { + context.Request.RefreshToken = refreshToken; + } + else + { + // If we got here, either the refresh token was not redacted, or nothing was found in the refresh token cookie. + // If OpenIddict found a refresh token, it could be an old token that is potentially still valid. For security + // reasons, we cannot accept that; at this point, we expect the refresh tokens to be explicitly redacted. + context.Request.RefreshToken = null; + } + + + return ValueTask.CompletedTask; + } + + /// + /// This is invoked when extracting the auth context for a client request. + /// + public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessAuthenticationContext context) + { + // For the back-office client, this only happens when an access token is sent to the API. + if (context.AccessToken != RedactedTokenValue) + { + return ValueTask.CompletedTask; + } + + if (TryGetCookie(AccessTokenCookieKey, out var accessToken)) + { + context.AccessToken = accessToken; + } + + return ValueTask.CompletedTask; + } + + public void Handle(UserLogoutSuccessNotification notification) + { + HttpContext? context = _httpContextAccessor.HttpContext; + if (context is null) + { + // For some reason there is no ambient HTTP context, so we can't clean up the cookies. + // This is OK, because the tokens in the cookies have already been revoked at user sign-out, + // so the cookie clean-up is mostly cosmetic. + return; + } + + context.Response.Cookies.Delete(AccessTokenCookieKey); + context.Response.Cookies.Delete(RefreshTokenCookieKey); + } + + private HttpContext GetHttpContext() + => _httpContextAccessor.GetRequiredHttpContext(); + + private void SetCookie(HttpContext httpContext, string key, string value) + { + var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider); + + var cookieOptions = new CookieOptions + { + // Prevent the client-side scripts from accessing the cookie. + HttpOnly = true, + + // Mark the cookie as essential to the application, to enforce it despite any + // data collection consent options. This aligns with how ASP.NET Core Identity + // does when writing cookies for cookie authentication. + IsEssential = true, + + // Cookie path must be root for optimal security. + Path = "/", + + // For optimal security, the cooke must be secure. However, Umbraco allows for running development + // environments over HTTP, so we need to take that into account here. + // Thus, we will make the cookie secure if: + // - HTTPS is explicitly enabled by config (default for production environments), or + // - The current request is over HTTPS (meaning the environment supports it regardless of config). + Secure = _globalSettings.UseHttps || httpContext.Request.IsHttps, + + // SameSite is configurable (see BackOfficeTokenCookieSettings for defaults): + SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite), + }; + + httpContext.Response.Cookies.Delete(key, cookieOptions); + httpContext.Response.Cookies.Append(key, cookieValue, cookieOptions); + } + + private bool TryGetCookie(string key, [NotNullWhen(true)] out string? value) + { + if (GetHttpContext().Request.Cookies.TryGetValue(key, out var cookieValue)) + { + value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider); + return true; + } + + value = null; + return false; + } + + private static SameSiteMode ParseSameSiteMode(string sameSiteMode) => + Enum.TryParse(sameSiteMode, ignoreCase: true, out SameSiteMode result) + ? result + : throw new ArgumentException($"The provided {nameof(sameSiteMode)} value could not be parsed into as SameSiteMode value.", nameof(sameSiteMode)); +} diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index 0139bb61ce91..92a24046b238 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; using Umbraco.Extensions; @@ -28,6 +29,11 @@ public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder) private static void ConfigureOpenIddict(IUmbracoBuilder builder) { + // Optionally hide tokens from the back-office. + var hideBackOfficeTokens = (builder.Config + .GetSection(Constants.Configuration.ConfigBackOfficeTokenCookie) + .Get() ?? new BackOfficeTokenCookieSettings()).Enabled; + builder.Services.AddOpenIddict() // Register the OpenIddict server components. .AddServer(options => @@ -113,6 +119,22 @@ private static void ConfigureOpenIddict(IUmbracoBuilder builder) { configuration.UseSingletonHandler().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1); }); + + if (hideBackOfficeTokens) + { + options.AddEventHandler(configuration => + { + configuration + .UseSingletonHandler() + .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse.Descriptor.Order - 1); + }); + options.AddEventHandler(configuration => + { + configuration + .UseSingletonHandler() + .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest.Descriptor.Order + 1); + }); + } }) // Register the OpenIddict validation components. @@ -137,9 +159,25 @@ private static void ConfigureOpenIddict(IUmbracoBuilder builder) { configuration.UseSingletonHandler().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1); }); + + if (hideBackOfficeTokens) + { + options.AddEventHandler(configuration => + { + configuration + .UseSingletonHandler() + // IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string. + .SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1); + }); + } }); builder.Services.AddRecurringBackgroundJob(); builder.Services.ConfigureOptions(); + + if (hideBackOfficeTokens) + { + builder.AddNotificationHandler(); + } } } diff --git a/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs b/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs new file mode 100644 index 000000000000..4019c425474f --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Typed configuration options for back-office token cookie settings. +/// +[UmbracoOptions(Constants.Configuration.ConfigBackOfficeTokenCookie)] +[Obsolete("This will be replaced with a different authentication scheme. Scheduled for removal in Umbraco 18.")] +public class BackOfficeTokenCookieSettings +{ + private const bool StaticEnabled = false; + + private const string StaticSameSite = "Strict"; + + /// + /// Gets or sets a value indicating whether to enable access and refresh tokens in cookies. + /// + [DefaultValue(StaticEnabled)] + [Obsolete("This is only configurable in Umbraco 16. Scheduled for removal in Umbraco 17.")] + public bool Enabled { get; set; } = StaticEnabled; + + /// + /// Gets or sets a value indicating whether the cookie SameSite configuration. + /// + /// + /// Valid values are "Unspecified", "None", "Lax" and "Strict" (default). + /// + [DefaultValue(StaticSameSite)] + public string SameSite { get; set; } = StaticSameSite; +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 8504210504d8..7e506a978f7f 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -64,6 +64,7 @@ public static class Configuration public const string ConfigWebhook = ConfigPrefix + "Webhook"; public const string ConfigWebhookPayloadType = ConfigWebhook + ":PayloadType"; public const string ConfigCache = ConfigPrefix + "Cache"; + public const string ConfigBackOfficeTokenCookie = ConfigSecurity + ":BackOfficeTokenCookie"; public static class NamedOptions { diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index cc9b03e65b7d..81b387a74b3e 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -86,7 +86,8 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder) .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.Web.UI.Client/.github/README.md b/src/Umbraco.Web.UI.Client/.github/README.md index cf76fd68ef7c..d677ad9e0dfd 100644 --- a/src/Umbraco.Web.UI.Client/.github/README.md +++ b/src/Umbraco.Web.UI.Client/.github/README.md @@ -26,6 +26,7 @@ If you have an existing Vite server running, you can run the task **Backoffice A ### Run a Front-end server against a local Umbraco instance #### 1. Configure Umbraco instance + Enable the front-end server communicating with the Backend server(Umbraco instance) you need need to correct the `appsettings.json` of your project. For code contributions use the backend project of `/src/Umbraco.Web.UI`. @@ -38,7 +39,11 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json` "BackOfficeHost": "http://localhost:5173", "AuthorizeCallbackPathName": "/oauth_complete", "AuthorizeCallbackLogoutPathName": "/logout", - "AuthorizeCallbackErrorPathName": "/error", + "AuthorizeCallbackErrorPathName": "/error",, + "BackOfficeTokenCookie": { + "Enabled": true, + "SameSite": "None" + } }, }, } @@ -46,10 +51,15 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json` This will override the backoffice host URL, enabling the Client to run from a different origin. +> [!NOTE] +> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately. + #### 2. Start Umbraco + Then start the backend server by running the command: `dotnet run` in the `/src/Umbraco.Web.UI` folder. #### 3. Start Frontend server + Now start the frontend server by running the command: `npm run dev:server` in the `/src/Umbraco.Web.UI.Client` folder. Finally open `http://localhost:5173` in your browser. diff --git a/src/Umbraco.Web.UI.Client/src/external/openid/src/xhr.ts b/src/Umbraco.Web.UI.Client/src/external/openid/src/xhr.ts index fc32dbf8cba0..0ef5981a5b0a 100644 --- a/src/Umbraco.Web.UI.Client/src/external/openid/src/xhr.ts +++ b/src/Umbraco.Web.UI.Client/src/external/openid/src/xhr.ts @@ -35,6 +35,7 @@ export class FetchRequestor extends Requestor { const requestInit: RequestInit = {}; requestInit.method = settings.method; requestInit.mode = 'cors'; + requestInit.credentials = settings.credentials ?? 'include'; if (settings.data) { if (settings.method && settings.method.toUpperCase() === 'POST') { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index fc6f5d1ab2d2..7a9d0bbf2e8d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -365,6 +365,7 @@ export class UmbAuthFlow { const token = await this.performWithFreshTokens(); const request = new Request(this.#unlink_endpoint, { method: 'POST', + credentials: 'include', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ loginProvider, providerKey }), }); @@ -458,6 +459,7 @@ export class UmbAuthFlow { const token = await this.performWithFreshTokens(); const request = await fetch(`${this.#link_key_endpoint}?provider=${provider}`, { + credentials: 'include', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/http-client/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/http-client/index.ts index 450649ecb292..26ee2b570c3f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/http-client/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/http-client/index.ts @@ -1 +1,14 @@ -export { client as umbHttpClient } from '@umbraco-cms/backoffice/external/backend-api'; +import { client } from '@umbraco-cms/backoffice/external/backend-api'; + +/** + * Pre-configure the client with default credentials for cookie-based authentication. + * This ensures all requests include cookies by default, which is required for + * cookie-based authentication in Umbraco 17.0+. + * + * Extensions using this client will automatically get credentials: 'include'. + */ +client.setConfig({ + credentials: 'include', +}); + +export { client as umbHttpClient }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts index 2be9e84fa792..dce562bc3a80 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/try-execute/tryXhrRequest.function.ts @@ -44,6 +44,7 @@ function createXhrRequest(options: XhrRequestOptions): UmbCancelablePromise(async (resolve, reject, onCancel) => { const xhr = new XMLHttpRequest(); xhr.open(options.method, `${baseUrl}${options.url}`, true); + xhr.withCredentials = options.withCredentials ?? true; // Set default headers if (options.token) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts index 60e415e0d5b0..887f09c18016 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/types.ts @@ -7,6 +7,7 @@ export interface XhrRequestOptions extends UmbTryExecuteOptions { baseUrl?: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; url: string; + withCredentials?: boolean; body?: unknown; token?: string | (() => undefined | string | Promise); headers?: Record; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts index 4d2cde0ffc1b..ae14cd16a9f1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts @@ -22,6 +22,7 @@ export class UmbDocumentPermissionServerDataSource { this.#host, fetch(`/umbraco/management/api/v1/document/${id}/permissions`, { method: 'GET', + credentials: 'include', headers: { 'Content-Type': 'application/json', }, From d4d4b8a50a6855e0218abb4550194396ce294363 Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:19:33 +0100 Subject: [PATCH 144/155] Content Type Designer: Always register root route to support drag-and-drop into empty Generic tab. (#20809) Always register root route to enable drag-drop on empty Generic tab. --- .../content-type-design-editor.element.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts index 7eb5696694de..7dc47bef73e9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -207,15 +207,16 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements }); } + routes.push({ + path: 'root', + component: () => import('./content-type-design-editor-tab.element.js'), + setup: (component) => { + this.#currentTabComponent = component as UmbContentTypeDesignEditorTabElement; + this.#currentTabComponent.containerId = null; + }, + }); + if (this._hasRootGroups || this._tabs.length === 0) { - routes.push({ - path: 'root', - component: () => import('./content-type-design-editor-tab.element.js'), - setup: (component) => { - this.#currentTabComponent = component as UmbContentTypeDesignEditorTabElement; - this.#currentTabComponent.containerId = null; - }, - }); routes.push({ path: '', pathMatch: 'full', From f0752234124ce84d76cd22edcd0b85426d6fe919 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 13 Nov 2025 09:32:18 +0100 Subject: [PATCH 145/155] Relations: Exclude the relate parent on delete relation type from checks for related documents and media on delete, when disable delete with references is enabled (closes #20803) (#20811) * Exclude the relate parent on delete relation type from checks for related documents and media on delete, when disable delete with references is enabled. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Applied suggestions from code review. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Persistence/Querying/IQuery.cs | 8 ++++++ .../Services/ContentEditingService.cs | 3 +++ .../Services/ContentEditingServiceBase.cs | 27 +++++++++++++++++-- src/Umbraco.Core/Services/IRelationService.cs | 17 +++++++++++- .../Services/MediaEditingService.cs | 3 +++ src/Umbraco.Core/Services/RelationService.cs | 19 ++++++++++--- .../Persistence/Querying/Query.cs | 22 ++++++++++++--- .../ContentEditingServiceTests.Delete.cs | 18 +++++++++++++ .../Services/ContentEditingServiceTests.cs | 4 +-- 9 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Querying/IQuery.cs b/src/Umbraco.Core/Persistence/Querying/IQuery.cs index 8803d69fc048..e87ec77b86ab 100644 --- a/src/Umbraco.Core/Persistence/Querying/IQuery.cs +++ b/src/Umbraco.Core/Persistence/Querying/IQuery.cs @@ -30,6 +30,14 @@ public interface IQuery /// This instance so calls to this method are chainable IQuery WhereIn(Expression> fieldSelector, IEnumerable? values); + /// + /// Adds a where-not-in clause to the query + /// + /// + /// + /// This instance so calls to this method are chainable + IQuery WhereNotIn(Expression> fieldSelector, IEnumerable? values) => throw new NotImplementedException(); // TODO (V18): Remove default implementation. + /// /// Adds a set of OR-ed where clauses to the query. /// diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index 9b4f92615b12..783421a2c280 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -62,6 +62,9 @@ public ContentEditingService( _languageService = languageService; } + /// + protected override string? RelateParentOnDeleteAlias => Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; + public Task GetAsync(Guid key) { IContent? content = ContentService.GetById(key); diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs index 15a4b9670e3b..41e1adf7b3f1 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs @@ -75,6 +75,11 @@ protected ContentEditingServiceBase( protected TContentTypeService ContentTypeService { get; } + /// + /// Gets the alias used to relate the parent entity when handling content (document or media) delete operations. + /// + protected virtual string? RelateParentOnDeleteAlias => null; + protected async Task> MapCreate(ContentCreationModelBase contentCreationModelBase) where TContentCreateResult : ContentCreateResultBase, new() { @@ -202,9 +207,27 @@ private async Task(status, content); } - if (disabledWhenReferenced && _relationService.IsRelated(content.Id, RelationDirectionFilter.Child)) + if (disabledWhenReferenced) { - return Attempt.FailWithStatus(referenceFailStatus, content); + // When checking if an item is related, we may need to exclude the "relate parent on delete" relation type, as this prevents + // deleting from the recycle bin. + int[]? excludeRelationTypeIds = null; + if (string.IsNullOrWhiteSpace(RelateParentOnDeleteAlias) is false) + { + IRelationType? relateParentOnDeleteRelationType = _relationService.GetRelationTypeByAlias(RelateParentOnDeleteAlias); + if (relateParentOnDeleteRelationType is not null) + { + excludeRelationTypeIds = [relateParentOnDeleteRelationType.Id]; + } + } + + if (_relationService.IsRelated( + content.Id, + RelationDirectionFilter.Child, + excludeRelationTypeIds: excludeRelationTypeIds)) + { + return Attempt.FailWithStatus(referenceFailStatus, content); + } } var userId = await GetUserIdAsync(userKey); diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 828abc1fd124..7ddae1394ded 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -297,9 +297,24 @@ public interface IRelationService : IService ///
    /// Id of an object to check relations for /// Indicates whether to check for relations as parent, child or in either direction. - /// Returns True if any relations exists with the given Id, otherwise False + /// Returns True if any relations exists with the given Id, otherwise False. + [Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")] bool IsRelated(int id, RelationDirectionFilter directionFilter); + /// + /// Checks whether any relations exists for the passed in Id and direction. + /// + /// Id of an object to check relations for + /// Indicates whether to check for relations as parent, child or in either direction. + /// A collection of relation type Ids to include consideration in the relation checks. + /// A collection of relation type Ids to exclude from consideration in the relation checks. + /// If no relation type Ids are provided in includeRelationTypeIds or excludeRelationTypeIds, all relation type Ids are considered. + /// Returns True if any relations exists with the given Id, otherwise False. + bool IsRelated(int id, RelationDirectionFilter directionFilter, int[]? includeRelationTypeIds = null, int[]? excludeRelationTypeIds = null) +#pragma warning disable CS0618 // Type or member is obsolete + => IsRelated(id, directionFilter); +#pragma warning restore CS0618 // Type or member is obsolete + /// /// Checks whether two items are related /// diff --git a/src/Umbraco.Core/Services/MediaEditingService.cs b/src/Umbraco.Core/Services/MediaEditingService.cs index 6947e1dc5d0e..ec9177c150bd 100644 --- a/src/Umbraco.Core/Services/MediaEditingService.cs +++ b/src/Umbraco.Core/Services/MediaEditingService.cs @@ -43,6 +43,9 @@ public MediaEditingService( contentTypeFilters) => _logger = logger; + /// + protected override string? RelateParentOnDeleteAlias => Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + public Task GetAsync(Guid key) { IMedia? media = ContentService.GetById(key); diff --git a/src/Umbraco.Core/Services/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs index 35adb2e217bc..ed1d03df7029 100644 --- a/src/Umbraco.Core/Services/RelationService.cs +++ b/src/Umbraco.Core/Services/RelationService.cs @@ -485,11 +485,14 @@ public bool HasRelations(IRelationType relationType) return _relationRepository.Get(query).Any(); } - /// - public bool IsRelated(int id) => IsRelated(id, RelationDirectionFilter.Any); + [Obsolete("No longer used in Umbraco, please the overload taking all parameters. Scheduled for removal in Umbraco 19.")] + public bool IsRelated(int id) => IsRelated(id, RelationDirectionFilter.Any, null, null); + + [Obsolete("Please the overload taking all parameters. Scheduled for removal in Umbraco 18.")] + public bool IsRelated(int id, RelationDirectionFilter directionFilter) => IsRelated(id, directionFilter, null, null); /// - public bool IsRelated(int id, RelationDirectionFilter directionFilter) + public bool IsRelated(int id, RelationDirectionFilter directionFilter, int[]? includeRelationTypeIds = null, int[]? excludeRelationTypeIds = null) { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IQuery query = Query(); @@ -502,6 +505,16 @@ public bool IsRelated(int id, RelationDirectionFilter directionFilter) _ => throw new ArgumentOutOfRangeException(nameof(directionFilter)), }; + if (includeRelationTypeIds is not null && includeRelationTypeIds.Length > 0) + { + query = query.WhereIn(x => x.RelationTypeId, includeRelationTypeIds); + } + + if (excludeRelationTypeIds is not null && excludeRelationTypeIds.Length > 0) + { + query = query.WhereNotIn(x => x.RelationTypeId, excludeRelationTypeIds); + } + return _relationRepository.Get(query).Any(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs b/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs index 88d1326f44dd..c66e121cf736 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs @@ -22,7 +22,7 @@ public class Query : IQuery /// public virtual IQuery Where(Expression>? predicate) { - if (predicate == null) + if (predicate is null) { return this; } @@ -38,7 +38,7 @@ public virtual IQuery Where(Expression>? predicate) /// public virtual IQuery WhereIn(Expression>? fieldSelector, IEnumerable? values) { - if (fieldSelector == null) + if (fieldSelector is null) { return this; } @@ -49,12 +49,28 @@ public virtual IQuery WhereIn(Expression>? fieldSelector, IEn return this; } + /// + /// Adds a where-not-in clause to the query. + /// + public virtual IQuery WhereNotIn(Expression>? fieldSelector, IEnumerable? values) + { + if (fieldSelector is null) + { + return this; + } + + var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); + var whereExpression = expressionHelper.Visit(fieldSelector); + _wheres.Add(new Tuple(whereExpression + " NOT IN (@values)", new object[] { new { values } })); + return this; + } + /// /// Adds a set of OR-ed where clauses to the query. /// public virtual IQuery WhereAny(IEnumerable>>? predicates) { - if (predicates == null) + if (predicates is null) { return this; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs index 95a7807fad33..8c0544421fa7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs @@ -34,6 +34,24 @@ public async Task Cannot_Delete_When_Content_Is_Related_As_A_Child_And_Configure Assert.IsNotNull(subpage); } + [Test] + [ConfigureBuilder(ActionName = nameof(ConfigureDisableDeleteWhenReferenced))] + public async Task Can_Delete_When_Content_Is_Related_To_Parent_For_Restore_And_Configured_To_Disable_When_Related() + { + var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey); + Assert.IsTrue(moveAttempt.Success); + + // Setup a relation where the page being deleted is related to it's parent (created as the location to restore to). + Relate(Subpage2, Subpage, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias); + var result = await ContentEditingService.DeleteFromRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + + // re-get and verify is deleted + var subpage = await ContentEditingService.GetAsync(Subpage.Key); + Assert.IsNull(subpage); + } + [Test] [ConfigureBuilder(ActionName = nameof(ConfigureDisableDeleteWhenReferenced))] public async Task Can_Delete_When_Content_Is_Related_As_A_Parent_And_Configured_To_Disable_When_Related() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.cs index 4fca28f2ffd1..4c8508cfa684 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.cs @@ -16,9 +16,9 @@ public partial class ContentEditingServiceTests : ContentEditingServiceTestsBase [SetUp] public void Setup() => ContentRepositoryBase.ThrowOnWarning = true; - public void Relate(IContent parent, IContent child) + public void Relate(IContent parent, IContent child, string relationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias) { - var relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias); + var relatedContentRelType = RelationService.GetRelationTypeByAlias(relationTypeAlias); var relation = RelationService.Relate(parent.Id, child.Id, relatedContentRelType); RelationService.Save(relation); From b65d2b0ec7e6222a3a4d45cb7f1f801973f566b6 Mon Sep 17 00:00:00 2001 From: NguyenThuyLan <116753400+NguyenThuyLan@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:00:21 +0700 Subject: [PATCH 146/155] Collection view test: update changes for v17 (#20812) update extension for collection view test Co-authored-by: Lan Nguyen Thuy --- .../my-collection-view/my-collection-view.js | 271 ++++++------------ .../my-collection-view.js.map | 2 +- .../my-collection-view/umbraco-package.json | 2 +- 3 files changed, 82 insertions(+), 193 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js index 38ef5a40de09..0629033aef97 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js @@ -1,217 +1,106 @@ -import { UMB_DOCUMENT_COLLECTION_CONTEXT as T, UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN as P } from "@umbraco-cms/backoffice/document"; -import { css as D, state as d, customElement as x, html as n } from "@umbraco-cms/backoffice/external/lit"; -import { UmbLitElement as E } from "@umbraco-cms/backoffice/lit-element"; -import { UmbTextStyles as A } from "@umbraco-cms/backoffice/style"; -import { fromCamelCase as O } from "@umbraco-cms/backoffice/utils"; -var S = Object.defineProperty, $ = Object.getOwnPropertyDescriptor, y = (e) => { - throw TypeError(e); -}, c = (e, t, a, s) => { - for (var r = s > 1 ? void 0 : s ? $(t, a) : t, p = e.length - 1, m; p >= 0; p--) - (m = e[p]) && (r = (s ? m(t, a, r) : m(r)) || r); - return s && r && S(t, a, r), r; -}, f = (e, t, a) => t.has(e) || y("Cannot " + a), u = (e, t, a) => (f(e, t, "read from private field"), t.get(e)), _ = (e, t, a) => t.has(e) ? y("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(e) : t.set(e, a), I = (e, t, a, s) => (f(e, t, "write to private field"), t.set(e, a), a), h = (e, t, a) => (f(e, t, "access private method"), a), b, o, l, w, g, v, C; -let i = class extends E { +import { css as u, state as d, customElement as _, html as n } from "@umbraco-cms/backoffice/external/lit"; +import { UmbLitElement as f } from "@umbraco-cms/backoffice/lit-element"; +import { UMB_DOCUMENT_COLLECTION_CONTEXT as v } from "@umbraco-cms/backoffice/document"; +var y = Object.defineProperty, w = Object.getOwnPropertyDescriptor, h = (t) => { + throw TypeError(t); +}, m = (t, e, a, s) => { + for (var i = s > 1 ? void 0 : s ? w(e, a) : e, o = t.length - 1, l; o >= 0; o--) + (l = t[o]) && (i = (s ? l(e, a, i) : l(i)) || i); + return s && i && y(e, a, i), i; +}, b = (t, e, a) => e.has(t) || h("Cannot " + a), C = (t, e, a) => e.has(t) ? h("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, a), E = (t, e, a) => (b(t, e, "access private method"), a), c, p; +let r = class extends f { constructor() { - super(), _(this, l), this._tableColumns = [], _(this, b, [ - { - name: this.localize.term("general_name"), - alias: "name", - elementName: "umb-document-table-column-name", - allowSorting: !0 - }, - { - name: this.localize.term("content_publishStatus"), - alias: "state", - elementName: "umb-document-table-column-state", - allowSorting: !1 - } - ]), this._tableItems = [], _(this, o), this.consumeContext(T, (e) => { - I(this, o, e), e?.setupView(this), this.observe( - e?.workspacePathBuilder, - (t) => { - this._workspacePathBuilder = t, u(this, o) && h(this, l, v).call(this, u(this, o).getItems()); - }, - "observePath" - ), h(this, l, w).call(this); + super(), C(this, c), this._columns = [], this._items = [], this.consumeContext(v, (t) => { + t?.setupView(this), this.observe(t?.userDefinedProperties, (e) => { + E(this, c, p).call(this, e); + }), this.observe(t?.items, (e) => { + this._items = e; + }); }); } render() { - return n` - + return this._items === void 0 ? n`

    Not found...

    ` : n` +
    - ${this._tableColumns.map( - (e) => n`` - )} + ${this._columns.map((t) => n``)} - ${this._tableItems.map( - (e) => n` + ${this._items.map( + (t) => n` - ${this._tableColumns.map((t) => { - const s = e.data.find((r) => r.columnAlias === t.alias)?.value ?? ""; - return t.alias === "name" && s?.item ? n`` : t.alias === "state" && s?.item ? n`` : t.alias === "entityActions" ? n`` : n``; + ${this._columns.map((e) => { + switch (e.alias) { + case "name": + return n``; + case "entityActions": + return n``; + default: + const a = t.values.find((s) => s.alias === e.alias)?.value ?? ""; + return n``; + } })} ` )} -
    ${e.name}${t.name}
    ${s.item.name}${s.item.state}${s}${t.variants[0].name}${a}
    `; + + `; } }; -b = /* @__PURE__ */ new WeakMap(); -o = /* @__PURE__ */ new WeakMap(); -l = /* @__PURE__ */ new WeakSet(); -w = function() { - u(this, o) && (this.observe( - u(this, o).userDefinedProperties, - (e) => { - this._userDefinedProperties = e, h(this, l, g).call(this); - }, - "_observeUserDefinedProperties" - ), this.observe( - u(this, o).items, - (e) => { - this._items = e, h(this, l, v).call(this, this._items); - }, - "_observeItems" - )); -}; -g = function() { - if (this._userDefinedProperties && this._userDefinedProperties.length > 0) { - const e = this._userDefinedProperties.map((t) => ({ - name: this.localize.string(t.header), - alias: t.alias, - elementName: t.elementName, - labelTemplate: t.nameTemplate, - allowSorting: !0 - })); - this._tableColumns = [ - ...u(this, b), - ...e, - { name: "", alias: "entityActions", align: "right" } - ]; - } -}; -v = function(e) { - this._tableItems = e.map((t) => { - if (!t.unique) throw new Error("Item id is missing."); - const a = this._tableColumns?.map((s) => { - if (s.alias === "entityActions") - return { - columnAlias: "entityActions", - value: n`` - }; - const r = t.unique && this._workspacePathBuilder ? this._workspacePathBuilder({ entityType: t.entityType }) + P.generateLocal({ - unique: t.unique - }) : ""; - return { - columnAlias: s.alias, - value: s.elementName ? { item: t, editPath: r } : h(this, l, C).call(this, t, s.alias) - }; - }) ?? []; - return { - id: t.unique, - icon: t.documentType.icon, - entityType: "document", - data: a - }; - }); -}; -C = function(e, t) { - switch (t) { - case "contentTypeAlias": - return e.contentTypeAlias; - case "createDate": - return e.createDate.toLocaleString(); - case "creator": - case "owner": - return e.creator; - case "name": - return e.name; - case "state": - return O(e.state); - case "published": - return e.state !== "Draft" ? "True" : "False"; - case "sortOrder": - return e.sortOrder; - case "updateDate": - return e.updateDate.toLocaleString(); - case "updater": - return e.updater; - default: - return e.values.find((a) => a.alias === t)?.value ?? ""; - } +c = /* @__PURE__ */ new WeakSet(); +p = function(t = []) { + const e = [ + { name: "Name", alias: "name" }, + { name: "State", alias: "state" } + ], a = t.map((s) => ({ + name: s.nameTemplate ?? s.alias, + alias: s.alias + })); + this._columns = [...e, ...a, { name: "", alias: "entityActions", align: "right" }]; }; -i.styles = [ - A, - D` - :host { - display: block; - box-sizing: border-box; - height: auto; - width: 100%; - padding: var(--uui-size-space-3) 0; - } - - .container { - display: flex; - justify-content: center; - align-items: center; - } - - :host { +r.styles = u` + :host { display: block; width: 100%; overflow-x: auto; - } - table { - width: 100%; - border-collapse: collapse; - font-size: 14px; - } - th, - td { - padding: 6px 10px; - border: 1px solid #ddd; - white-space: nowrap; - } - th { - background: #f8f8f8; - font-weight: 600; - } - a { - color: var(--uui-color-interactive, #0366d6); - text-decoration: none; - } - a:hover { - text-decoration: underline; - } - ` -]; -c([ - d() -], i.prototype, "_workspacePathBuilder", 2); -c([ - d() -], i.prototype, "_userDefinedProperties", 2); -c([ - d() -], i.prototype, "_items", 2); -c([ + font-family: sans-serif; + } + table { + width: 100%; + border-collapse: collapse; + } + th, + td { + padding: 6px 10px; + border: 1px solid #ddd; + white-space: nowrap; + } + th { + background: #f8f8f8; + font-weight: 600; + } + a { + color: var(--uui-color-interactive, #0366d6); + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + `; +m([ d() -], i.prototype, "_tableColumns", 2); -c([ +], r.prototype, "_columns", 2); +m([ d() -], i.prototype, "_tableItems", 2); -i = c([ - x("my-document-table-collection-view") -], i); -const z = i; +], r.prototype, "_items", 2); +r = m([ + _("my-document-table-collection-view") +], r); +const O = r; export { - i as MyDocumentTableCollectionViewElement, - z as default + r as MyDocumentTableCollectionViewElement, + O as default }; //# sourceMappingURL=my-collection-view.js.map diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js.map b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js.map index b613f3d7c86c..1b84b4da576a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js.map +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/my-collection-view.js.map @@ -1 +1 @@ -{"version":3,"file":"my-collection-view.js","sources":["../../my-collection-view/src/my-collection-view.ts"],"sourcesContent":["import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/document';\r\nimport type { UmbDocumentCollectionItemModel } from '@umbraco-cms/backoffice/document';\r\nimport { UMB_DOCUMENT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/document';\r\nimport type { UmbCollectionColumnConfiguration } from '@umbraco-cms/backoffice/collection';\r\nimport { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';\r\nimport { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';\r\nimport { UmbTextStyles } from '@umbraco-cms/backoffice/style';\r\nimport type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';\r\nimport { fromCamelCase } from '@umbraco-cms/backoffice/utils';\r\nimport type {\r\n\tUmbTableColumn,\r\n\tUmbTableItem,\r\n} from '@umbraco-cms/backoffice/components';\r\n\r\n@customElement('my-document-table-collection-view')\r\nexport class MyDocumentTableCollectionViewElement extends UmbLitElement {\r\n\t@state()\r\n\tprivate _workspacePathBuilder?: UmbModalRouteBuilder;\r\n\r\n\t@state()\r\n\tprivate _userDefinedProperties?: Array;\r\n\r\n\t@state()\r\n\tprivate _items?: Array;\r\n\r\n\t@state()\r\n\tprivate _tableColumns: Array = [];\r\n\r\n\t#systemColumns: Array = [\r\n\t\t{\r\n\t\t\tname: this.localize.term('general_name'),\r\n\t\t\talias: 'name',\r\n\t\t\telementName: 'umb-document-table-column-name',\r\n\t\t\tallowSorting: true,\r\n\t\t},\r\n\t\t{\r\n\t\t\tname: this.localize.term('content_publishStatus'),\r\n\t\t\talias: 'state',\r\n\t\t\telementName: 'umb-document-table-column-state',\r\n\t\t\tallowSorting: false,\r\n\t\t},\r\n\t];\r\n\r\n\t@state()\r\n\tprivate _tableItems: Array = [];\r\n\r\n\t#collectionContext?: typeof UMB_DOCUMENT_COLLECTION_CONTEXT.TYPE;\r\n\r\n\tconstructor() {\r\n\t\tsuper();\r\n\r\n\t\tthis.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {\r\n\t\t\tthis.#collectionContext = collectionContext;\r\n\t\t\tcollectionContext?.setupView(this);\r\n\t\t\tthis.observe(\r\n\t\t\t\tcollectionContext?.workspacePathBuilder,\r\n\t\t\t\t(builder) => {\r\n\t\t\t\t\tthis._workspacePathBuilder = builder;\r\n\t\t\t\t\tif (this.#collectionContext) {\r\n\t\t\t\t\t\tthis.#createTableItems(this.#collectionContext.getItems());\r\n\t\t\t\t\t}\r\n\t\t\t\t},\r\n\t\t\t\t'observePath',\r\n\t\t\t);\r\n\t\t\tthis.#observeCollectionContext();\r\n\t\t});\r\n\t}\r\n\r\n\t#observeCollectionContext() {\r\n\t\tif (!this.#collectionContext) return;\r\n\r\n\t\tthis.observe(\r\n\t\t\tthis.#collectionContext.userDefinedProperties,\r\n\t\t\t(userDefinedProperties) => {\r\n\t\t\t\tthis._userDefinedProperties = userDefinedProperties;\r\n\t\t\t\tthis.#createTableHeadings();\r\n\t\t\t},\r\n\t\t\t'_observeUserDefinedProperties',\r\n\t\t);\r\n\r\n\t\tthis.observe(\r\n\t\t\tthis.#collectionContext.items,\r\n\t\t\t(items) => {\r\n\t\t\t\tthis._items = items;\r\n\t\t\t\tthis.#createTableItems(this._items);\r\n\t\t\t},\r\n\t\t\t'_observeItems',\r\n\t\t);\r\n\t}\r\n\r\n\t#createTableHeadings() {\r\n\t\tif (this._userDefinedProperties && this._userDefinedProperties.length > 0) {\r\n\t\t\tconst userColumns: Array = this._userDefinedProperties.map((item) => {\r\n\t\t\t\treturn {\r\n\t\t\t\t\tname: this.localize.string(item.header),\r\n\t\t\t\t\talias: item.alias,\r\n\t\t\t\t\telementName: item.elementName,\r\n\t\t\t\t\tlabelTemplate: item.nameTemplate,\r\n\t\t\t\t\tallowSorting: true,\r\n\t\t\t\t};\r\n\t\t\t});\r\n\r\n\t\t\tthis._tableColumns = [\r\n\t\t\t\t...this.#systemColumns,\r\n\t\t\t\t...userColumns,\r\n\t\t\t\t{ name: '', alias: 'entityActions', align: 'right' },\r\n\t\t\t];\r\n\t\t}\r\n\t}\r\n\r\n\t#createTableItems(items: Array) {\r\n\t\tthis._tableItems = items.map((item) => {\r\n\t\t\tif (!item.unique) throw new Error('Item id is missing.');\r\n\r\n\t\t\tconst data =\r\n\t\t\t\tthis._tableColumns?.map((column) => {\r\n\t\t\t\t\tif (column.alias === 'entityActions') {\r\n\t\t\t\t\t\treturn {\r\n\t\t\t\t\t\t\tcolumnAlias: 'entityActions',\r\n\t\t\t\t\t\t\tvalue: html``,\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tconst editPath =\r\n\t\t\t\t\t\titem.unique && this._workspacePathBuilder\r\n\t\t\t\t\t\t\t? this._workspacePathBuilder({ entityType: item.entityType }) +\r\n\t\t\t\t\t\t\tUMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({\r\n\t\t\t\t\t\t\t\tunique: item.unique,\r\n\t\t\t\t\t\t\t})\r\n\t\t\t\t\t\t\t: '';\r\n\r\n\t\t\t\t\treturn {\r\n\t\t\t\t\t\tcolumnAlias: column.alias,\r\n\t\t\t\t\t\tvalue: column.elementName ? { item, editPath } : this.#getPropertyValueByAlias(item, column.alias),\r\n\t\t\t\t\t};\r\n\t\t\t\t}) ?? [];\r\n\r\n\t\t\treturn {\r\n\t\t\t\tid: item.unique,\r\n\t\t\t\ticon: item.documentType.icon,\r\n\t\t\t\tentityType: 'document',\r\n\t\t\t\tdata: data,\r\n\t\t\t};\r\n\t\t});\r\n\t}\r\n\r\n\t#getPropertyValueByAlias(item: UmbDocumentCollectionItemModel, alias: string) {\r\n\tswitch (alias) {\r\n\t\tcase 'contentTypeAlias':\r\n\t\t\treturn item.contentTypeAlias;\r\n\t\tcase 'createDate':\r\n\t\t\treturn item.createDate.toLocaleString();\r\n\t\tcase 'creator':\r\n\t\tcase 'owner':\r\n\t\t\treturn item.creator;\r\n\t\tcase 'name':\r\n\t\t\treturn item.name;\r\n\t\tcase 'state':\r\n\t\t\treturn fromCamelCase(item.state);\r\n\t\tcase 'published':\r\n\t\t\treturn item.state !== 'Draft' ? 'True' : 'False';\r\n\t\tcase 'sortOrder':\r\n\t\t\treturn item.sortOrder;\r\n\t\tcase 'updateDate':\r\n\t\t\treturn item.updateDate.toLocaleString();\r\n\t\tcase 'updater':\r\n\t\t\treturn item.updater;\r\n\t\tdefault:\r\n\t\t\treturn item.values.find((value) => value.alias === alias)?.value ?? '';\r\n\t}\r\n}\r\n\r\n\toverride render() {\r\n\t\treturn html`\r\n\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t${this._tableColumns.map(\r\n\t\t\t\t\t\t\t(col) => html``,\r\n\t\t\t\t\t\t)}\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t${this._tableItems.map(\r\n\t\t\t\t\t\t\t(item) => html`\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t${this._tableColumns.map((col) => {\r\n\t\t\t\t\t\t\t\tconst cell = item.data.find((d) => d.columnAlias === col.alias);\r\n\t\t\t\t\t\t\t\tconst val = cell?.value ?? '';\r\n\t\t\t\t\t\t\t\tif (col.alias === 'name' && val?.item) {\r\n\t\t\t\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\tif (col.alias === 'state' && val?.item) {\r\n\t\t\t\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\tif (col.alias === 'entityActions') {\r\n\t\t\t\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\t\t\t})}\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t`,\r\n\t\t\t\t\t\t)}\r\n\t\t\t\t\r\n\t\t\t
    ${col.name}
    ${val.item.name}${val.item.state}${val}
    `;\r\n\t}\r\n\r\n\tstatic override styles = [\r\n\t\tUmbTextStyles,\r\n\t\tcss`\r\n\t\t\t:host {\r\n\t\t\t\tdisplay: block;\r\n\t\t\t\tbox-sizing: border-box;\r\n\t\t\t\theight: auto;\r\n\t\t\t\twidth: 100%;\r\n\t\t\t\tpadding: var(--uui-size-space-3) 0;\r\n\t\t\t}\r\n\r\n\t\t\t.container {\r\n\t\t\t\tdisplay: flex;\r\n\t\t\t\tjustify-content: center;\r\n\t\t\t\talign-items: center;\r\n\t\t\t}\r\n\r\n\t\t\t:host {\r\n\t\t\tdisplay: block;\r\n\t\t\twidth: 100%;\r\n\t\t\toverflow-x: auto;\r\n\t\t\t}\r\n\t\t\ttable {\r\n\t\t\t\twidth: 100%;\r\n\t\t\t\tborder-collapse: collapse;\r\n\t\t\t\tfont-size: 14px;\r\n\t\t\t}\r\n\t\t\tth,\r\n\t\t\ttd {\r\n\t\t\t\tpadding: 6px 10px;\r\n\t\t\t\tborder: 1px solid #ddd;\r\n\t\t\t\twhite-space: nowrap;\r\n\t\t\t}\r\n\t\t\tth {\r\n\t\t\t\tbackground: #f8f8f8;\r\n\t\t\t\tfont-weight: 600;\r\n\t\t\t}\r\n\t\t\ta {\r\n\t\t\t\tcolor: var(--uui-color-interactive, #0366d6);\r\n\t\t\t\ttext-decoration: none;\r\n\t\t\t}\r\n\t\t\ta:hover {\r\n\t\t\t\ttext-decoration: underline;\r\n\t\t\t}\r\n\t\t\t`,\r\n\t];\r\n}\r\n\r\nexport default MyDocumentTableCollectionViewElement;\r\n\r\ndeclare global {\r\n\tinterface HTMLElementTagNameMap {\r\n\t\t'my-document-table-collection-view': MyDocumentTableCollectionViewElement;\r\n\t}\r\n}\r\n"],"names":["_systemColumns","_collectionContext","_MyDocumentTableCollectionViewElement_instances","observeCollectionContext_fn","createTableHeadings_fn","createTableItems_fn","getPropertyValueByAlias_fn","MyDocumentTableCollectionViewElement","UmbLitElement","__privateAdd","UMB_DOCUMENT_COLLECTION_CONTEXT","collectionContext","__privateSet","builder","__privateGet","__privateMethod","html","col","item","val","d","userDefinedProperties","items","userColumns","data","column","editPath","UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN","alias","fromCamelCase","value","UmbTextStyles","css","__decorateClass","state","customElement","MyDocumentTableCollectionViewElement$1"],"mappings":";;;;;;;;;;;wXAAAA,GAAAC,GAAAC,GAAAC,GAAAC,GAAAC,GAAAC;AAeO,IAAMC,IAAN,cAAmDC,EAAc;AAAA,EAiCvE,cAAc;AACb,UAAA,GAlCKC,EAAA,MAAAP,CAAA,GAWN,KAAQ,gBAAuC,CAAA,GAE/CO,EAAA,MAAAT,GAAwC;AAAA,MACvC;AAAA,QACC,MAAM,KAAK,SAAS,KAAK,cAAc;AAAA,QACvC,OAAO;AAAA,QACP,aAAa;AAAA,QACb,cAAc;AAAA,MAAA;AAAA,MAEf;AAAA,QACC,MAAM,KAAK,SAAS,KAAK,uBAAuB;AAAA,QAChD,OAAO;AAAA,QACP,aAAa;AAAA,QACb,cAAc;AAAA,MAAA;AAAA,IACf,CACD,GAGA,KAAQ,cAAmC,CAAA,GAE3CS,EAAA,MAAAR,CAAA,GAKC,KAAK,eAAeS,GAAiC,CAACC,MAAsB;AAC3E,MAAAC,EAAA,MAAKX,GAAqBU,CAAA,GAC1BA,GAAmB,UAAU,IAAI,GACjC,KAAK;AAAA,QACJA,GAAmB;AAAA,QACnB,CAACE,MAAY;AACZ,eAAK,wBAAwBA,GACzBC,QAAKb,CAAA,KACRc,EAAA,MAAKb,GAAAG,CAAA,EAAL,KAAA,MAAuBS,EAAA,MAAKb,CAAA,EAAmB,SAAA,CAAS;AAAA,QAE1D;AAAA,QACA;AAAA,MAAA,GAEDc,EAAA,MAAKb,GAAAC,CAAA,EAAL,KAAA,IAAA;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EA2GS,SAAS;AACjB,WAAOa;AAAA;AAAA;AAAA;AAAA,QAID,KAAK,cAAc;AAAA,MACpB,CAACC,MAAQD,0BAA6BC,EAAI,SAAS,MAAM,KAAKA,EAAI,IAAI;AAAA,IAAA,CACtE;AAAA;AAAA;AAAA;AAAA,OAIA,KAAK,YAAY;AAAA,MACjB,CAACC,MAASF;AAAA;AAAA,UAEP,KAAK,cAAc,IAAI,CAACC,MAAQ;AAElC,cAAME,IADOD,EAAK,KAAK,KAAK,CAACE,MAAMA,EAAE,gBAAgBH,EAAI,KAAK,GAC5C,SAAS;AAC3B,eAAIA,EAAI,UAAU,UAAUE,GAAK,OACzBH,gBAAmBG,EAAI,YAAY,GAAG,IAAIA,EAAI,KAAK,IAAI,cAE3DF,EAAI,UAAU,WAAWE,GAAK,OAC1BH,QAAWG,EAAI,KAAK,KAAK,UAE7BF,EAAI,UAAU,kBACVD,0CAEDA,QAAWG,CAAG;AAAA,MACtB,CAAC,CAAC;AAAA;AAAA;AAAA,IAAA,CAGF;AAAA;AAAA;AAAA,EAGN;AAgDD;AAlOCnB,IAAA,oBAAA,QAAA;AAkBAC,IAAA,oBAAA,QAAA;AA/BMC,IAAA,oBAAA,QAAA;AAqDNC,IAAyB,WAAG;AAC3B,EAAKW,QAAKb,CAAA,MAEV,KAAK;AAAA,IACJa,QAAKb,CAAA,EAAmB;AAAA,IACxB,CAACoB,MAA0B;AAC1B,WAAK,yBAAyBA,GAC9BN,EAAA,MAAKb,GAAAE,CAAA,EAAL,KAAA,IAAA;AAAA,IACD;AAAA,IACA;AAAA,EAAA,GAGD,KAAK;AAAA,IACJU,QAAKb,CAAA,EAAmB;AAAA,IACxB,CAACqB,MAAU;AACV,WAAK,SAASA,GACdP,EAAA,MAAKb,GAAAG,CAAA,EAAL,WAAuB,KAAK,MAAA;AAAA,IAC7B;AAAA,IACA;AAAA,EAAA;AAEF;AAEAD,IAAoB,WAAG;AACtB,MAAI,KAAK,0BAA0B,KAAK,uBAAuB,SAAS,GAAG;AAC1E,UAAMmB,IAAqC,KAAK,uBAAuB,IAAI,CAACL,OACpE;AAAA,MACN,MAAM,KAAK,SAAS,OAAOA,EAAK,MAAM;AAAA,MACtC,OAAOA,EAAK;AAAA,MACZ,aAAaA,EAAK;AAAA,MAClB,eAAeA,EAAK;AAAA,MACpB,cAAc;AAAA,IAAA,EAEf;AAED,SAAK,gBAAgB;AAAA,MACpB,GAAGJ,EAAA,MAAKd,CAAA;AAAA,MACR,GAAGuB;AAAA,MACH,EAAE,MAAM,IAAI,OAAO,iBAAiB,OAAO,QAAA;AAAA,IAAQ;AAAA,EAErD;AACD;AAEAlB,IAAiB,SAACiB,GAA8C;AAC/D,OAAK,cAAcA,EAAM,IAAI,CAACJ,MAAS;AACtC,QAAI,CAACA,EAAK,OAAQ,OAAM,IAAI,MAAM,qBAAqB;AAEvD,UAAMM,IACL,KAAK,eAAe,IAAI,CAACC,MAAW;AACnC,UAAIA,EAAO,UAAU;AACpB,eAAO;AAAA,UACN,aAAa;AAAA,UACb,OAAOT;AAAA,iBACGE,CAAI;AAAA,QAAA;AAIhB,YAAMQ,IACLR,EAAK,UAAU,KAAK,wBACjB,KAAK,sBAAsB,EAAE,YAAYA,EAAK,WAAA,CAAY,IAC5DS,EAAyC,cAAc;AAAA,QACtD,QAAQT,EAAK;AAAA,MAAA,CACb,IACC;AAEJ,aAAO;AAAA,QACN,aAAaO,EAAO;AAAA,QACpB,OAAOA,EAAO,cAAc,EAAE,MAAAP,GAAM,UAAAQ,EAAA,IAAaX,EAAA,MAAKb,GAAAI,CAAA,EAAL,KAAA,MAA8BY,GAAMO,EAAO,KAAA;AAAA,MAAA;AAAA,IAE9F,CAAC,KAAK,CAAA;AAEP,WAAO;AAAA,MACN,IAAIP,EAAK;AAAA,MACT,MAAMA,EAAK,aAAa;AAAA,MACxB,YAAY;AAAA,MACZ,MAAAM;AAAA,IAAA;AAAA,EAEF,CAAC;AACF;AAEAlB,IAAwB,SAACY,GAAsCU,GAAe;AAC9E,UAAQA,GAAA;AAAA,IACP,KAAK;AACJ,aAAOV,EAAK;AAAA,IACb,KAAK;AACJ,aAAOA,EAAK,WAAW,eAAA;AAAA,IACxB,KAAK;AAAA,IACL,KAAK;AACJ,aAAOA,EAAK;AAAA,IACb,KAAK;AACJ,aAAOA,EAAK;AAAA,IACb,KAAK;AACJ,aAAOW,EAAcX,EAAK,KAAK;AAAA,IAChC,KAAK;AACJ,aAAOA,EAAK,UAAU,UAAU,SAAS;AAAA,IAC1C,KAAK;AACJ,aAAOA,EAAK;AAAA,IACb,KAAK;AACJ,aAAOA,EAAK,WAAW,eAAA;AAAA,IACxB,KAAK;AACJ,aAAOA,EAAK;AAAA,IACb;AACC,aAAOA,EAAK,OAAO,KAAK,CAACY,MAAUA,EAAM,UAAUF,CAAK,GAAG,SAAS;AAAA,EAAA;AAEvE;AA5JarB,EAiMI,SAAS;AAAA,EACxBwB;AAAA,EACAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2CD;AA5OQC,EAAA;AAAA,EADPC,EAAA;AAAM,GADK3B,EAEJ,WAAA,yBAAA,CAAA;AAGA0B,EAAA;AAAA,EADPC,EAAA;AAAM,GAJK3B,EAKJ,WAAA,0BAAA,CAAA;AAGA0B,EAAA;AAAA,EADPC,EAAA;AAAM,GAPK3B,EAQJ,WAAA,UAAA,CAAA;AAGA0B,EAAA;AAAA,EADPC,EAAA;AAAM,GAVK3B,EAWJ,WAAA,iBAAA,CAAA;AAkBA0B,EAAA;AAAA,EADPC,EAAA;AAAM,GA5BK3B,EA6BJ,WAAA,eAAA,CAAA;AA7BIA,IAAN0B,EAAA;AAAA,EADNE,EAAc,mCAAmC;AAAA,GACrC5B,CAAA;AAiPb,MAAA6B,IAAe7B;"} \ No newline at end of file +{"version":3,"file":"my-collection-view.js","sources":["../../my-collection-view/src/my-collection-view.ts"],"sourcesContent":["import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';\r\nimport { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';\r\nimport { UMB_DOCUMENT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/document';\r\nimport type { UmbDocumentCollectionItemModel } from '@umbraco-cms/backoffice/document';\r\nimport type { UmbCollectionColumnConfiguration } from '@umbraco-cms/backoffice/collection';\r\n\r\n@customElement('my-document-table-collection-view')\r\nexport class MyDocumentTableCollectionViewElement extends UmbLitElement {\r\n\r\n\t@state() private _columns: Array<{ name: string; alias: string; align?: string }> = [];\r\n\t@state() private _items?: Array = [];\r\n\r\n\tconstructor() {\r\n\t\tsuper();\r\n\r\n\t\tthis.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {\r\n\t\t\tcollectionContext?.setupView(this);\r\n\r\n\t\t\tthis.observe(collectionContext?.userDefinedProperties, (props) => {\r\n\t\t\t\tthis.#createColumns(props);\r\n\t\t\t});\r\n\r\n\t\t\tthis.observe(collectionContext?.items, (items) => {\r\n\t\t\t\tthis._items = items;\r\n\t\t\t});\r\n\t\t});\r\n\t}\r\n\r\n\t#createColumns(userProps: Array = []) {\r\n\t\tconst baseCols = [\r\n\t\t\t{ name: 'Name', alias: 'name' },\r\n\t\t\t{ name: 'State', alias: 'state' },\r\n\t\t];\r\n\t\tconst userCols = userProps.map((p) => ({\r\n\t\t\tname: p.nameTemplate ?? p.alias,\r\n\t\t\talias: p.alias,\r\n\t\t}));\r\n\t\tthis._columns = [...baseCols, ...userCols, { name: '', alias: 'entityActions', align: 'right' }];\r\n\t}\r\n\r\n\toverride render() {\r\n if (this._items === undefined) return html`

    Not found...

    `;\r\n\t\treturn html`\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t${this._columns.map((col) => html``)}\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t${this._items.map(\r\n\t\t\t(item) => html`\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t${this._columns.map((col) => {\r\n\t\t\t\tswitch (col.alias) {\r\n\t\t\t\t\tcase 'name':\r\n\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\tcase 'entityActions':\r\n\t\t\t\t\t\treturn html``;\r\n\t\t\t\t\tdefault:\r\n\t\t\t\t\t\tconst val = item.values.find((v) => v.alias === col.alias)?.value ?? '';\r\n\t\t\t\t\t\treturn html``;\r\n\t\t\t\t}\r\n\t\t\t})}\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t`\r\n\t\t)}\r\n\t\t\t\t\r\n\t\t\t
    ${col.name}
    ${item.variants[0].name}${val}
    \r\n\t\t`;\r\n\t}\r\n\r\n\tstatic override styles = css`\r\n\t\t:host {\r\n\t\t\tdisplay: block;\r\n\t\t\twidth: 100%;\r\n\t\t\toverflow-x: auto;\r\n\t\t\tfont-family: sans-serif;\r\n\t\t}\r\n\t\ttable {\r\n\t\t\twidth: 100%;\r\n\t\t\tborder-collapse: collapse;\r\n\t\t}\r\n\t\tth,\r\n\t\ttd {\r\n\t\t\tpadding: 6px 10px;\r\n\t\t\tborder: 1px solid #ddd;\r\n\t\t\twhite-space: nowrap;\r\n\t\t}\r\n\t\tth {\r\n\t\t\tbackground: #f8f8f8;\r\n\t\t\tfont-weight: 600;\r\n\t\t}\r\n\t\ta {\r\n\t\t\tcolor: var(--uui-color-interactive, #0366d6);\r\n\t\t\ttext-decoration: none;\r\n\t\t}\r\n\t\ta:hover {\r\n\t\t\ttext-decoration: underline;\r\n\t\t}\r\n\t`;\r\n}\r\n\r\nexport default MyDocumentTableCollectionViewElement;\r\n\r\ndeclare global {\r\n\tinterface HTMLElementTagNameMap {\r\n\t\t'my-document-table-collection-view': MyDocumentTableCollectionViewElement;\r\n\t}\r\n}\r\n"],"names":["_MyDocumentTableCollectionViewElement_instances","createColumns_fn","MyDocumentTableCollectionViewElement","UmbLitElement","__privateAdd","UMB_DOCUMENT_COLLECTION_CONTEXT","collectionContext","props","__privateMethod","items","html","col","item","val","v","userProps","baseCols","userCols","p","css","__decorateClass","state","customElement","MyDocumentTableCollectionViewElement$1"],"mappings":";;;;;;;;;8OAAAA,GAAAC;AAOO,IAAMC,IAAN,cAAmDC,EAAc;AAAA,EAKvE,cAAc;AACb,UAAA,GANKC,EAAA,MAAAJ,CAAA,GAEG,KAAQ,WAAmE,CAAA,GAC3E,KAAQ,SAAiD,CAAA,GAKjE,KAAK,eAAeK,GAAiC,CAACC,MAAsB;AAC3E,MAAAA,GAAmB,UAAU,IAAI,GAEjC,KAAK,QAAQA,GAAmB,uBAAuB,CAACC,MAAU;AACjE,QAAAC,EAAA,MAAKR,MAAL,KAAA,MAAoBO,CAAA;AAAA,MACrB,CAAC,GAED,KAAK,QAAQD,GAAmB,OAAO,CAACG,MAAU;AACjD,aAAK,SAASA;AAAA,MACf,CAAC;AAAA,IACF,CAAC;AAAA,EACF;AAAA,EAcS,SAAS;AACX,WAAI,KAAK,WAAW,SAAkBC,yBACrCA;AAAA;AAAA;AAAA;AAAA,QAID,KAAK,SAAS,IAAI,CAACC,MAAQD,0BAA6BC,EAAI,SAAS,MAAM,KAAKA,EAAI,IAAI,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA,OAIjG,KAAK,OAAO;AAAA,MAChB,CAACC,MAASF;AAAA;AAAA,UAEH,KAAK,SAAS,IAAI,CAACC,MAAQ;AACjC,gBAAQA,EAAI,OAAA;AAAA,UACX,KAAK;AACJ,mBAAOD,oBAAuBE,EAAK,SAAS,CAAC,EAAE,IAAI;AAAA,UACpD,KAAK;AACJ,mBAAOF;AAAA,UACR;AACC,kBAAMG,IAAMD,EAAK,OAAO,KAAK,CAACE,MAAMA,EAAE,UAAUH,EAAI,KAAK,GAAG,SAAS;AACrE,mBAAOD,QAAWG,CAAG;AAAA,QAAA;AAAA,MAExB,CAAC,CAAC;AAAA;AAAA;AAAA,IAAA,CAGF;AAAA;AAAA;AAAA;AAAA,EAIF;AA+BD;AA9FOb,IAAA,oBAAA,QAAA;AAqBNC,IAAc,SAACc,IAAqD,IAAI;AACvE,QAAMC,IAAW;AAAA,IAChB,EAAE,MAAM,QAAQ,OAAO,OAAA;AAAA,IACvB,EAAE,MAAM,SAAS,OAAO,QAAA;AAAA,EAAQ,GAE3BC,IAAWF,EAAU,IAAI,CAACG,OAAO;AAAA,IACtC,MAAMA,EAAE,gBAAgBA,EAAE;AAAA,IAC1B,OAAOA,EAAE;AAAA,EAAA,EACR;AACF,OAAK,WAAW,CAAC,GAAGF,GAAU,GAAGC,GAAU,EAAE,MAAM,IAAI,OAAO,iBAAiB,OAAO,SAAS;AAChG;AA/BYf,EAiEI,SAASiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA/DRC,EAAA;AAAA,EAAhBC,EAAA;AAAM,GAFKnB,EAEK,WAAA,YAAA,CAAA;AACAkB,EAAA;AAAA,EAAhBC,EAAA;AAAM,GAHKnB,EAGK,WAAA,UAAA,CAAA;AAHLA,IAANkB,EAAA;AAAA,EADNE,EAAc,mCAAmC;AAAA,GACrCpB,CAAA;AAgGb,MAAAqB,IAAerB;"} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/umbraco-package.json b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/umbraco-package.json index 7cbeb21c79e5..1cef3bf27535 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/umbraco-package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/my-collection-view/umbraco-package.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "../../umbraco-package-schema.json", "name": "My Collection View", "version": "0.1.0", From eeda55c06f1f9b4215323472d1a06e10e2c60ada Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 13 Nov 2025 12:25:17 +0100 Subject: [PATCH 147/155] Preview: Add validation support to Save and Preview button (closes #20616) (#20805) * chore(mock): adds missing try/catch around document lookup * fix: lets the 'save and preview' button extend the 'save' button to follow the same logic in terms of when it enables/disabled - it did not have much logic before * fix: runs validation from the server when save and previewing to ensure the UI shows what is missing --- .../handlers/document/detail.handlers.ts | 11 +++++-- .../save-and-preview.action.ts | 31 +++---------------- .../workspace/document-workspace.context.ts | 9 +++++- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts index 9fbaebc38955..4b02a555c65c 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/detail.handlers.ts @@ -1,4 +1,5 @@ const { rest } = window.MockServiceWorker; +import type { UmbMockDocumentModel } from '../../data/document/document.data.js'; import { umbDocumentMockDb } from '../../data/document/document.db.js'; import { items as referenceData } from '../../data/tracked-reference.data.js'; import { UMB_SLUG } from './slug.js'; @@ -77,8 +78,14 @@ export const detailHandlers = [ rest.get(umbracoPath(`${UMB_SLUG}/:id/available-segment-options`), (req, res, ctx) => { const id = req.params.id as string; if (!id) return res(ctx.status(400)); - const document = umbDocumentMockDb.detail.read(id); - if (!document) return res(ctx.status(404)); + + let document: UmbMockDocumentModel | null = null; + + try { + document = umbDocumentMockDb.detail.read(id); + } catch { + return res(ctx.status(404)); + } const availableSegments = document.variants.filter((v) => !!v.segment).map((v) => v.segment!) ?? []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/preview/workspace-action/save-and-preview.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/preview/workspace-action/save-and-preview.action.ts index e622c57bf675..adac4f628664 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/preview/workspace-action/save-and-preview.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/preview/workspace-action/save-and-preview.action.ts @@ -1,30 +1,9 @@ -import { UmbDocumentUserPermissionCondition } from '../../user-permissions/document/conditions/document-user-permission.condition.js'; -import { UMB_USER_PERMISSION_DOCUMENT_UPDATE } from '../../user-permissions/document/constants.js'; -import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbDocumentSaveWorkspaceAction } from '../../workspace/actions/save.action.js'; -export class UmbDocumentSaveAndPreviewWorkspaceAction extends UmbWorkspaceActionBase { - constructor(host: UmbControllerHost, args: any) { - super(host, args); - - /* The action is disabled by default because the onChange callback - will first be triggered when the condition is changed to permitted */ - this.disable(); - - const condition = new UmbDocumentUserPermissionCondition(host, { - host, - config: { - alias: 'Umb.Condition.UserPermission.Document', - allOf: [UMB_USER_PERMISSION_DOCUMENT_UPDATE], - }, - onChange: () => { - if (condition.permitted) { - this.enable(); - } else { - this.disable(); - } - }, - }); +export class UmbDocumentSaveAndPreviewWorkspaceAction extends UmbDocumentSaveWorkspaceAction { + override async execute() { + await this._retrieveWorkspaceContext; + await this._workspaceContext?.saveAndPreview(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 50d45001ff16..6dadb4ae7c95 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -332,7 +332,14 @@ export class UmbDocumentWorkspaceContext firstVariantId = UmbVariantId.FromString(selected[0]); const variantIds = [firstVariantId]; const saveData = await this._data.constructData(variantIds); - await this.runMandatoryValidationForSaveData(saveData); + + // Run mandatory validation (checks for name, etc.) + await this.runMandatoryValidationForSaveData(saveData, variantIds); + + // Ask server to validate and show validation tooltips (like the Save action does) + await this.askServerToValidate(saveData, variantIds); + + // Perform save await this.performCreateOrUpdate(variantIds, saveData); } From 714fbf31192c39970432295089996a1eaa7ce5eb Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Thu, 13 Nov 2025 12:55:45 +0100 Subject: [PATCH 148/155] Keyboard navigation: Return to opening element after modal close (#20782) Removed the detroy from the modelContext. It being destroyed prevented the uui-button getting into focus again after closing the modal. --- .../packages/core/modal/controller/open-modal.controller.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/controller/open-modal.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/controller/open-modal.controller.ts index 994cea878055..f10cbc3e73d9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/controller/open-modal.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/controller/open-modal.controller.ts @@ -21,9 +21,7 @@ export class UmbOpenModalController extends UmbControllerBase { const modalContext = modalManagerContext.open(this, modalAlias, args); - return await modalContext.onSubmit().finally(() => { - this.destroy(); - }); + return await modalContext.onSubmit(); } } From 617d3014794a8144c9e7dc28c999824c639220f2 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:39:51 +0100 Subject: [PATCH 149/155] Hides the content files that come from the Microsoft.CodeAnalysis.Workspaces.Common package in the web.ui project in 17 (#20825) * Hide content files * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Andy Butland Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 2ade2ca396d8..a9c363c8a0cd 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -57,6 +57,11 @@ <_ContentIncludedByDefault Remove="umbraco\UmbracoBackOffice\Default.cshtml" /> + + + + + From bbd30363a2a357ba64fa35e1c5777e1253f7f425 Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:38:32 +0100 Subject: [PATCH 150/155] Media Picker: Remove duplicate loaders in media cards. (#20793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Removed isLoding condition from the rich media input and let the thumbnail handle the loader. * Removed unused import. * change loader and adjust lit property configuration * update reflect configuration --------- Co-authored-by: Niels Lyngsø --- .../components/imaging-thumbnail.element.ts | 31 ++++++++++--------- .../input-rich-media.element.ts | 19 +++++------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/imaging-thumbnail.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/imaging-thumbnail.element.ts index b23d79e80a4d..0e8c0eef8fb4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/imaging-thumbnail.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/imaging/components/imaging-thumbnail.element.ts @@ -12,8 +12,8 @@ export class UmbImagingThumbnailElement extends UmbLitElement { * The unique identifier for the media item. * @description This is also known as the media key and is used to fetch the resource. */ - @property() - unique = ''; + @property({ type: String }) + unique?: string; /** * The width of the thumbnail in pixels. @@ -34,19 +34,19 @@ export class UmbImagingThumbnailElement extends UmbLitElement { * @description The mode determines how the image is cropped. * @enum {UmbImagingCropMode} */ - @property() + @property({ type: String }) mode: UmbImagingCropMode = UmbImagingCropMode.MIN; /** * The alt text for the thumbnail. */ - @property() + @property({ type: String }) alt = ''; /** * The fallback icon for the thumbnail. */ - @property() + @property({ type: String }) icon = 'icon-picture'; /** @@ -54,11 +54,17 @@ export class UmbImagingThumbnailElement extends UmbLitElement { * @enum {'lazy' | 'eager'} * @default 'lazy' */ - @property() + @property({ type: String }) loading: (typeof HTMLImageElement)['prototype']['loading'] = 'lazy'; + /** + * External loading state (e.g., when parent is waiting for metadata) + */ + @property({ type: Boolean, reflect: false, attribute: 'external-loading' }) + externalLoading = false; + @state() - private _isLoading = true; + private _isLoading = false; @state() private _thumbnailUrl = ''; @@ -69,7 +75,7 @@ export class UmbImagingThumbnailElement extends UmbLitElement { override render() { return when( - this._isLoading, + this.externalLoading || this._isLoading, () => this.#renderLoading(), () => this.#renderThumbnail(), ); @@ -114,7 +120,7 @@ export class UmbImagingThumbnailElement extends UmbLitElement { } #renderLoading() { - return html`
    `; + return html``; } #renderThumbnail() { @@ -154,11 +160,8 @@ export class UmbImagingThumbnailElement extends UmbLitElement { } #loader { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - width: 100%; + font-size: 2em; + margin-bottom: 1em; } #figure { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts index bab80c11764b..6e432890caa3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts @@ -4,7 +4,7 @@ import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/constants.js'; import { UmbMediaPickerInputContext } from '../input-media/input-media.context.js'; import { UmbFileDropzoneItemStatus } from '@umbraco-cms/backoffice/dropzone'; import type { UmbDropzoneChangeEvent } from '@umbraco-cms/backoffice/dropzone'; -import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -409,18 +409,15 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin< #renderItem(item: UmbRichMediaCardModel) { if (!item.unique) return nothing; const href = this.readonly ? undefined : this._routeBuilder?.({ key: item.unique }); + return html` - ${when( - item.isLoading, - () => html``, - () => html` - - `, - )} + + ${this.#renderIsTrashed(item)} ${this.#renderActions(item)} `; From 8b076597b37ea9383f2d050aadfa956342e02dfd Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:37:54 +0100 Subject: [PATCH 151/155] Entity Sign: Improve Firefox visibility and add focus support. (#20733) * Chenged right and left position of the infobox. * Added focus support to open the modal. * Moved tabindex out the constructor and added support for enter and space keys. --- .../core/entity-sign/components/entity-sign-bundle.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-sign/components/entity-sign-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-sign/components/entity-sign-bundle.element.ts index 731e8ba9234d..a872df301c54 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-sign/components/entity-sign-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-sign/components/entity-sign-bundle.element.ts @@ -170,9 +170,9 @@ export class UmbEntitySignBundleElement extends UmbLitElement { .infobox { position: absolute; - top: 100%; + top: 0; margin-top: calc(-12px + var(--offset-h)); - left: 100%; + left: 19px; margin-left: -6px; background-color: transparent; padding: var(--uui-size-2); From e549217e66aee6c0f175e783b25425dafeeb2b23 Mon Sep 17 00:00:00 2001 From: Engiber Lozada <89547469+engijlr@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:42:48 +0100 Subject: [PATCH 152/155] Content Type Designer: Use input-with-alias and implement regex validation for Alias. (#20755) * Implemented input-with-alias in the content-type-design-editor. * Added auto-generate-alias property to the input and revert deletion of checkAliasAutoGenerate method. * Added form-validation-message. * Added validation to the input-with-alias element to avoid special characters. --- .../src/assets/lang/en.ts | 1 + .../src/assets/lang/es.ts | 1 + ...ent-type-design-editor-property.element.ts | 151 ++++++------------ .../input-with-alias.element.ts | 27 +++- 4 files changed, 81 insertions(+), 99 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index a04d3fe3cc3c..839dfc9e7ec8 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2246,6 +2246,7 @@ export default { rangeExceeds: 'The low value must not exceed the high value.', invalidExtensions: 'One or more of the extensions are invalid.', allowedExtensions: 'Allowed extensions are:', + aliasInvalidFormat: 'Special characters are not allowed in alias', disallowedExtensions: 'Disallowed extensions are:', }, healthcheck: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/es.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/es.ts index 2e8ca5780b5c..6c525bd179dc 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/es.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/es.ts @@ -1393,6 +1393,7 @@ export default { invalidDate: 'Fecha no válida', invalidNumber: 'No es un número', invalidEmail: 'Email no válido', + aliasInvalidFormat: 'No se permiten caracteres especiales en el alias', }, healthcheck: { checkSuccessMessage: "El valor fue establecido en el valor recomendado: '%0%'.", diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-property.element.ts index 043d7cfd226b..0e322bd40339 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-property.element.ts @@ -2,13 +2,14 @@ import type { UmbContentTypePropertyStructureHelper } from '../../../structure/i import type { UmbContentTypeModel, UmbPropertyTypeModel, UmbPropertyTypeScaffoldModel } from '../../../types.js'; import { UmbPropertyTypeContext } from './content-type-design-editor-property.context.js'; import { css, html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { generateAlias } from '@umbraco-cms/backoffice/utils'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_EDIT_PROPERTY_TYPE_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/property-type'; -import type { UUIInputElement, UUIInputLockElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components'; +import { umbBindToValidation } from '@umbraco-cms/backoffice/validation'; /** * @element umb-content-type-design-editor-property @@ -20,7 +21,6 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { #context = new UmbPropertyTypeContext(this); #dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); #dataTypeUnique?: string; - #propertyUnique?: string; @property({ attribute: false }) public set propertyStructureHelper(value: UmbContentTypePropertyStructureHelper | undefined) { @@ -49,7 +49,6 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { this._property = value; this.#context.setAlias(value?.alias); this.#context.setLabel(value?.name); - this.#checkAliasAutoGenerate(this._property?.unique); this.#checkInherited(); this.#setDataType(this._property?.dataType?.unique); this.requestUpdate('property', oldValue); @@ -86,20 +85,6 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { @state() private _dataTypeName?: string; - @state() - private _aliasLocked = true; - - #autoGenerateAlias = true; - - #checkAliasAutoGenerate(unique: string | undefined) { - if (unique === this.#propertyUnique) return; - this.#propertyUnique = unique; - - if (this.#context.getAlias()) { - this.#autoGenerateAlias = false; - } - } - async #checkInherited() { if (this._propertyStructureHelper && this._property) { // We can first match with something if we have a name [NL] @@ -131,19 +116,6 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { this._propertyStructureHelper.partialUpdateProperty(this._property.unique, partialObject); } - #onToggleAliasLock(event: CustomEvent) { - if (!this.property?.alias && (event.target as UUIInputLockElement).locked) { - this.#autoGenerateAlias = true; - } else { - this.#autoGenerateAlias = false; - } - - this._aliasLocked = !this._aliasLocked; - if (!this._aliasLocked) { - (event.target as UUIInputElement)?.focus(); - } - } - async #setDataType(dataTypeUnique: string | undefined) { if (!dataTypeUnique) { this._dataTypeName = undefined; @@ -173,28 +145,23 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { this._propertyStructureHelper?.removeProperty(unique); } - #onAliasChanged(event: UUIInputEvent) { - this.#singleValueUpdate('alias', event.target.value.toString()); - } - - #onNameChanged(event: UUIInputEvent) { - const newName = event.target.value.toString(); - if (this.#autoGenerateAlias) { - this.#singleValueUpdate('alias', generateAlias(newName ?? '')); - } - this.#singleValueUpdate('name', newName); + #onNameAliasChange(e: InputEvent & { target: UmbInputWithAliasElement }) { + this.#partialUpdate({ + name: e.target.value, + alias: e.target.alias, + } as UmbPropertyTypeModel); } override render() { // TODO: Only show alias on label if user has access to DocumentType within settings: [NL] - return this._inherited ? this.renderInheritedProperty() : this.renderEditableProperty(); + return this._inherited ? this.#renderInheritedProperty() : this.#renderEditableProperty(); } - renderInheritedProperty() { + #renderInheritedProperty() { if (!this.property) return; if (this.sortModeActive) { - return this.renderSortableProperty(); + return this.#renderSortableProperty(); } else { return html`
    - ${this.renderPropertyTags()} + ${this.#renderPropertyName()} ${this.#renderPropertyTags()} ${this._inherited ? html` @@ -220,22 +187,27 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { } } - renderEditableProperty() { + #renderEditableProperty() { if (!this.property || !this.editPropertyTypePath) return; if (this.sortModeActive) { - return this.renderSortableProperty(); + return this.#renderSortableProperty(); } else { return html`