From f1f2f702c0d7420e61fefae97ee2dc2af8013da3 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 15 Oct 2025 20:22:58 +0200 Subject: [PATCH 01/28] Restored and obsoleted removed extension method AsEnumerableOfOne. --- src/Umbraco.Core/Extensions/ObjectExtensions.cs | 9 +++++++++ .../CoreThings/ObjectExtensionsTests.cs | 15 +++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index c53eee27a5..0497d2cec4 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -26,6 +26,15 @@ public static class ObjectExtensions private static readonly char[] _numberDecimalSeparatorsToNormalize = ['.', ',']; private static readonly CustomBooleanTypeConverter _customBooleanTypeConverter = new(); + /// + /// Returns an enumerable containing only the input object. + /// + /// The input object. + /// The type of the enumerable. + /// An enumerable containing only the input object. + [Obsolete("Please replace uses of this extension method with Enumerable.Repeat(input, 1). This extension method is no longer used in Umbraco and is scheduled for removal in Umbraco 19.")] + public static IEnumerable AsEnumerableOfOne(this T input) => Enumerable.Repeat(input, 1); + /// /// Returns an XML serialized safe string representation for the value and type. /// diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs index 1461364d02..38c233e2b9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs @@ -1,11 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Threading; using Microsoft.Extensions.Primitives; using NUnit.Framework; using Umbraco.Cms.Core.PropertyEditors; @@ -31,6 +27,17 @@ public class ObjectExtensionsTests private CultureInfo _savedCulture; + [Test] + public void Can_Create_Enumerable_Of_One() + { + var input = "hello"; +#pragma warning disable CS0618 // Type or member is obsolete + var result = input.AsEnumerableOfOne(); +#pragma warning restore CS0618 // Type or member is obsolete + Assert.AreEqual(1, result.Count()); + Assert.AreEqual("hello", result.First()); + } + [Test] public void Can_Convert_List_To_Enumerable() { From 3830d75413d5c9a160b55c11052a076cfa12af6c Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 16 Oct 2025 08:23:20 +0200 Subject: [PATCH 02/28] Obsolete `GetAtRoot` on `DocumentCache` (#20514) Obsolete GetAtRoot on DocumentCache. --- src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs | 3 +++ .../PropertyEditors/DateTimePropertyEditorTests.cs | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs b/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs index bebb34e67c..2d3e682f94 100644 --- a/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs +++ b/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs @@ -42,6 +42,9 @@ public sealed class DocumentCache : IPublishedContentCache public IPublishedContent? GetById(Guid contentId) => GetByIdAsync(contentId).GetAwaiter().GetResult(); + [Obsolete("This method is no longer used in Umbraco and is not defined on the interface. " + + "Any usage can be replaced with a call to IDocumentNavigationQueryService.TryGetRootKeys to retrieve the document keys, " + + "with each key passed to IPublishedContentCache.GetById to retrieve the IPublishedContent instances. Scheduled for removal in Umbraco 19.")] public IEnumerable GetAtRoot(bool preview, string? culture = null) { if (_documentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys) is false) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs index 7a1a5d0dac..2e5135f1ab 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs @@ -8,7 +8,6 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Infrastructure.HybridCache; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Testing; @@ -127,7 +126,6 @@ public class DateTimePropertyEditorTests : UmbracoIntegrationTest Assert.IsTrue(publishResult.Success); - var test = ((DocumentCache)PublishedContentCache).GetAtRoot(false); var publishedContent = await PublishedContentCache.GetByIdAsync(createContentResult.Result.Content.Key, false); Assert.IsNotNull(publishedContent); From 498754e1700a7b833de62bbef7359d9c03b6ea97 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Oct 2025 12:56:32 +0200 Subject: [PATCH 03/28] 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 85c0aaeb2a..405152917d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1378,6 +1378,11 @@ public class DocumentRepository : ContentRepositoryBase(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 654ea8bb54..52657a21e0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -541,6 +541,11 @@ public class MediaRepository : ContentRepositoryBase(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 0000000000..8bfe6859e7 --- /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 0000000000..d4de99daba --- /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 8beb7f2acc8d4c5e212b18e0d595142a7a08c203 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 16 Oct 2025 13:57:15 +0200 Subject: [PATCH 04/28] Collection menu item extension point (#20506) * add extension option for collection menu item * Add collection menu module export * remove unused css * register user collection menu item * register user collection menu * use collection modal for user picker * Delete user-picker-modal.element.ts * Update manifests.ts * explicit exports to avoid name collision * hack to avoid circular dependency * fix lint errors * fix missing const export * Update collection-menu-item.element.ts --- ...le-custom-picker-collection-data-source.ts | 16 ++- .../src/packages/core/collection/index.ts | 1 + .../core/collection/menu/constants.ts | 1 + .../default-collection-menu.element.ts | 18 ++- .../packages/core/collection/menu/index.ts | 1 + .../collection-menu-item-context.interface.ts | 17 +++ .../collection-menu-item.context.token.ts | 6 + .../menu-item/collection-menu-item.element.ts | 96 +++++++++++++++ .../collection/menu/menu-item/constants.ts | 1 + .../menu/menu-item/default/constants.ts | 1 + .../default-collection-menu-item.context.ts | 114 ++++++++++++++++++ .../default-collection-menu-item.element.ts | 80 ++++++++++++ .../menu/menu-item/default/index.ts | 2 + .../menu/menu-item/default/manifests.ts | 17 +++ .../collection-menu-item.extension.ts | 12 ++ .../menu/menu-item/extension/types.ts | 1 + .../core/collection/menu/menu-item/index.ts | 4 + .../collection/menu/menu-item/manifests.ts | 4 + .../user/user/collection/constants.ts | 1 + .../user/user/collection/manifests.ts | 6 +- .../user/user/collection/menu/constants.ts | 1 + .../user/user/collection/menu/manifests.ts | 23 ++++ .../menu/user-collection-menu-item.element.ts | 88 ++++++++++++++ .../packages/user/user/modals/manifests.ts | 1 - .../user-picker/user-picker-modal.element.ts | 98 --------------- .../user-picker/user-picker-modal.token.ts | 22 +++- 26 files changed, 512 insertions(+), 120 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item-context.interface.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.context.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/collection-menu-item.extension.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts index 6ab13a8459..34328f20b7 100644 --- a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts @@ -1,15 +1,22 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbCollectionFilterModel, UmbCollectionItemModel } from '@umbraco-cms/backoffice/collection'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; import type { UmbPickerCollectionDataSource, UmbPickerSearchableDataSource, } from '@umbraco-cms/backoffice/picker-data-source'; import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +interface ExampleCollectionItemModel extends UmbCollectionItemModel { + isPickable: boolean; +} + export class ExampleCustomPickerCollectionPropertyEditorDataSource extends UmbControllerBase - implements UmbPickerCollectionDataSource, UmbPickerSearchableDataSource + implements UmbPickerCollectionDataSource, UmbPickerSearchableDataSource { + collectionPickableFilter = (item: ExampleCollectionItemModel) => item.isPickable; + async requestCollection(args: UmbCollectionFilterModel) { // TODO: use args to filter/paginate etc console.log(args); @@ -41,35 +48,40 @@ export class ExampleCustomPickerCollectionPropertyEditorDataSource export { ExampleCustomPickerCollectionPropertyEditorDataSource as api }; -const customItems: Array = [ +const customItems: Array = [ { 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/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts index 5f32ec4ac1..20dce58e6a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts @@ -9,6 +9,7 @@ export * from './conditions/index.js'; export * from './constants.js'; export * from './default/collection-default.element.js'; export * from './global-components.js'; +export * from './menu/index.js'; export * from './workspace-view/index.js'; export * from './default/collection-default.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts index 5b0fcb8cb6..edcb448557 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts @@ -1 +1,2 @@ export { UMB_COLLECTION_MENU_CONTEXT } from './default/default-collection-menu.context.token.js'; +export * from './menu-item/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts index a1273792f3..c4f74e2085 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts @@ -1,7 +1,6 @@ import type { UmbCollectionItemModel } from '../../item/types.js'; import type { UmbCollectionSelectionConfiguration } from '../../types.js'; import type { UmbDefaultCollectionMenuContext } from './default-collection-menu.context.js'; -import { getItemFallbackIcon, getItemFallbackName } from '@umbraco-cms/backoffice/entity-item'; import { html, customElement, @@ -14,6 +13,8 @@ import { } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import '../menu-item/collection-menu-item.element.js'; + @customElement('umb-default-collection-menu') export class UmbDefaultCollectionMenuElement extends UmbLitElement { private _api: UmbDefaultCollectionMenuContext | undefined; @@ -115,16 +116,11 @@ export class UmbDefaultCollectionMenuElement extends UmbLitElement { #renderItem(item: UmbCollectionItemModel) { return html` - this._api?.selection.select(item.unique)} - @deselected=${() => this._api?.selection.deselect(item.unique)} - ?selected=${this._api?.selection.isSelected(item.unique)}> - ${item.icon - ? html`` - : html``} - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/index.ts new file mode 100644 index 0000000000..91173f9401 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/index.ts @@ -0,0 +1 @@ +export * from './menu-item/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item-context.interface.ts new file mode 100644 index 0000000000..22082ea596 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item-context.interface.ts @@ -0,0 +1,17 @@ +import type { UmbCollectionItemModel } from '../../item/types.js'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbContextMinimal } from '@umbraco-cms/backoffice/context-api'; + +export interface UmbCollectionMenuItemContext< + CollectionMenuItemType extends UmbCollectionItemModel = UmbCollectionItemModel, +> extends UmbApi, + UmbContextMinimal { + item: Observable; + isSelectable: Observable; + isSelected: Observable; + getItem(): CollectionMenuItemType | undefined; + setItem(item: CollectionMenuItemType | undefined): void; + select(): void; + deselect(): void; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.context.token.ts new file mode 100644 index 0000000000..0b36cdf25e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.context.token.ts @@ -0,0 +1,6 @@ +import type { UmbCollectionMenuItemContext } from './collection-menu-item-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_COLLECTION_MENU_ITEM_CONTEXT = new UmbContextToken( + 'UmbCollectionMenuItemContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.element.ts new file mode 100644 index 0000000000..90f12f474f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.element.ts @@ -0,0 +1,96 @@ +import { UmbDefaultCollectionMenuItemContext } from './default/index.js'; +import type { ManifestCollectionMenuItem } from './extension/types.js'; +import { customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { + UmbExtensionElementAndApiSlotElementBase, + umbExtensionsRegistry, +} from '@umbraco-cms/backoffice/extension-registry'; +import { createObservablePart } from '@umbraco-cms/backoffice/observable-api'; + +@customElement('umb-collection-menu-item') +export class UmbCollectionMenuItemElement extends UmbExtensionElementAndApiSlotElementBase { + @property({ type: String, reflect: true }) + get entityType() { + return this.#entityType; + } + set entityType(newVal) { + this.#entityType = newVal; + this.#observeEntityType(); + } + #entityType?: string; + + @property({ type: Object, attribute: false }) + override set props(newVal: Record | undefined) { + super.props = newVal; + this.#assignProps(); + } + override get props() { + return super.props; + } + + #observeEntityType() { + if (!this.#entityType) return; + + const filterByEntityType = (manifest: ManifestCollectionMenuItem) => { + if (!this.#entityType) return false; + return manifest.forEntityTypes.includes(this.#entityType); + }; + + // Check if we can find a matching collection menu item for the current entity type. + // If we can, we will use that one, if not we will render a fallback collection menu item. + this.observe( + // TODO: what should we do if there are multiple collection menu items for an entity type? + // This method gets all extensions based on a type, then filters them based on the entity type. and then we get the alias of the first one [NL] + createObservablePart( + umbExtensionsRegistry.byTypeAndFilter(this.getExtensionType(), filterByEntityType), + (x) => x[0]?.alias, + ), + (alias) => { + this.alias = alias; + + // If we don't find any registered collection menu items for this specific entity type, we will render a fallback collection menu item. + // This is on purpose not done with the extension initializer since we don't want to spin up a real extension unless we have to. + if (!alias) { + this.#renderFallbackItem(); + } + }, + 'umbObserveAlias', + ); + } + + #renderFallbackItem() { + // TODO: make creating of elements with apis a shared function. + const element = document.createElement('umb-default-collection-menu-item'); + const api = new UmbDefaultCollectionMenuItemContext(element); + element.api = api; + this._element = element; + this.#assignProps(); + this.requestUpdate('_element'); + } + + getExtensionType() { + return 'collectionMenuItem'; + } + + getDefaultElementName() { + return 'umb-default-collection-menu-item'; + } + + #assignProps() { + if (!this._element || !this.props) return; + + Object.keys(this.props).forEach((key) => { + (this._element as any)[key] = this.props![key]; + }); + } + + override getDefaultApiConstructor() { + return UmbDefaultCollectionMenuItemContext; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-collection-menu-item': UmbCollectionMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/constants.ts new file mode 100644 index 0000000000..a238646f6e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/constants.ts @@ -0,0 +1 @@ +export * from './default/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/constants.ts new file mode 100644 index 0000000000..84ef3547b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/constants.ts @@ -0,0 +1 @@ +export { UMB_COLLECTION_MENU_ITEM_DEFAULT_KIND_MANIFEST } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.context.ts new file mode 100644 index 0000000000..a56480c6ae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.context.ts @@ -0,0 +1,114 @@ +import type { UmbCollectionMenuItemContext } from '../collection-menu-item-context.interface.js'; +import { UMB_COLLECTION_MENU_ITEM_CONTEXT } from '../collection-menu-item.context.token.js'; +import type { UmbCollectionItemModel } from '../../../types.js'; +import type { ManifestCollectionMenuItem } from '../extension/types.js'; +import { UMB_COLLECTION_MENU_CONTEXT } from '../../default/default-collection-menu.context.token.js'; +import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { map } from '@umbraco-cms/backoffice/external/rxjs'; + +export class UmbDefaultCollectionMenuItemContext< + CollectionMenuItemType extends UmbCollectionItemModel = UmbCollectionItemModel, + > + extends UmbContextBase + implements UmbCollectionMenuItemContext +{ + #manifest?: ManifestCollectionMenuItem; + + protected readonly _item = new UmbObjectState(undefined); + readonly item = this._item.asObservable(); + + #isSelectable = new UmbBooleanState(false); + readonly isSelectable = this.#isSelectable.asObservable(); + + #isSelectableContext = new UmbBooleanState(false); + readonly isSelectableContext = this.#isSelectableContext.asObservable(); + + #isSelected = new UmbBooleanState(false); + readonly isSelected = this.#isSelected.asObservable(); + + #collectionMenuContext?: typeof UMB_COLLECTION_MENU_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host, UMB_COLLECTION_MENU_ITEM_CONTEXT); + this.#consumeContexts(); + } + + async #consumeContexts() { + this.consumeContext(UMB_COLLECTION_MENU_CONTEXT, (context) => { + this.#collectionMenuContext = context; + this.#observeIsSelectable(); + this.#observeIsSelected(); + }); + } + + public set manifest(manifest: ManifestCollectionMenuItem | undefined) { + if (this.#manifest === manifest) return; + this.#manifest = manifest; + } + public get manifest() { + return this.#manifest; + } + + public setItem(item: CollectionMenuItemType | undefined) { + this._item.setValue(item); + + if (item) { + this.#observeIsSelectable(); + this.#observeIsSelected(); + } + } + + public select() { + const unique = this.getItem()?.unique; + if (!unique) throw new Error('Could not select. Unique is missing'); + this.#collectionMenuContext?.selection.select(unique); + } + + public deselect() { + const unique = this.getItem()?.unique; + if (!unique) throw new Error('Could not deselect. Unique is missing'); + this.#collectionMenuContext?.selection.deselect(unique); + } + + getItem() { + return this._item.getValue(); + } + + #observeIsSelectable() { + if (!this.#collectionMenuContext) return; + const item = this.getItem(); + if (!item) return; + + this.observe( + this.#collectionMenuContext.selection.selectable, + (value) => { + this.#isSelectableContext.setValue(value); + + // If the collection menu is selectable, check if this item is selectable + if (value === true) { + const isSelectable = this.#collectionMenuContext?.selectableFilter?.(item) ?? true; + this.#isSelectable.setValue(isSelectable); + } + }, + 'observeIsSelectable', + ); + } + + #observeIsSelected() { + if (!this.#collectionMenuContext) return; + const unique = this.getItem()?.unique; + if (!unique) return; + + this.observe( + this.#collectionMenuContext.selection.selection.pipe(map((selection) => selection.includes(unique))), + (isSelected) => { + this.#isSelected.setValue(isSelected); + }, + 'observeIsSelected', + ); + } +} + +export { UmbDefaultCollectionMenuItemContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts new file mode 100644 index 0000000000..3dfed63ced --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts @@ -0,0 +1,80 @@ +import type { UmbCollectionItemModel } from '../../../item/types.js'; +import type { UmbCollectionMenuItemContext } from '../collection-menu-item-context.interface.js'; +import { html, state, property, customElement, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { getItemFallbackIcon, getItemFallbackName } from '@umbraco-cms/backoffice/entity-item'; + +@customElement('umb-default-collection-menu-item') +export class UmbDefaultCollectionMenuItemElement extends UmbLitElement { + @property({ type: Object, attribute: false }) + set item(newVal: UmbCollectionItemModel) { + this._item = newVal; + + if (this._item) { + this.#initItem(); + } + } + get item(): UmbCollectionItemModel | undefined { + return this._item; + } + protected _item?: UmbCollectionItemModel; + + @property({ type: Object, attribute: false }) + public set api(value: UmbCollectionMenuItemContext | undefined) { + this.#api = value; + + if (this.#api) { + this.observe(this.#api.isSelectable, (value) => (this._isSelectable = value)); + this.observe(this.#api.isSelected, (value) => (this._isSelected = value)); + this.#initItem(); + } + } + public get api(): UmbCollectionMenuItemContext | undefined { + return this.#api; + } + #api: UmbCollectionMenuItemContext | undefined; + + @state() + protected _isActive = false; + + @state() + protected _isSelected = false; + + @state() + protected _isSelectable = false; + + #initItem() { + if (!this.#api) return; + if (!this._item) return; + this.#api.setItem(this._item); + } + + override render() { + const item = this._item; + if (!item) return nothing; + + return html` + this.#api?.select()} + @deselected=${() => this.#api?.deselect()}> + ${item.icon + ? html`` + : html``} + + `; + } + + static override styles = [UmbTextStyles]; +} + +export { UmbDefaultCollectionMenuItemElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-default-collection-menu-item': UmbDefaultCollectionMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/index.ts new file mode 100644 index 0000000000..8ad34c7c7e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/index.ts @@ -0,0 +1,2 @@ +export { UmbDefaultCollectionMenuItemContext } from './default-collection-menu-item.context.js'; +export { UmbDefaultCollectionMenuItemElement } from './default-collection-menu-item.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/manifests.ts new file mode 100644 index 0000000000..0e55f0d326 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/manifests.ts @@ -0,0 +1,17 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_COLLECTION_MENU_ITEM_DEFAULT_KIND_MANIFEST: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.CollectionMenuItem.Default', + matchKind: 'default', + matchType: 'collectionMenuItem', + manifest: { + type: 'collectionMenuItem', + api: () => import('./default-collection-menu-item.context.js'), + element: () => import('./default-collection-menu-item.element.js'), + }, +}; + +export const manifests: Array = [ + UMB_COLLECTION_MENU_ITEM_DEFAULT_KIND_MANIFEST, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/collection-menu-item.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/collection-menu-item.extension.ts new file mode 100644 index 0000000000..4cc20eb332 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/collection-menu-item.extension.ts @@ -0,0 +1,12 @@ +import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestCollectionMenuItem extends ManifestElementAndApi { + type: 'collectionMenuItem'; + forEntityTypes: Array; +} + +declare global { + interface UmbExtensionManifestMap { + UmbCollectionMenuItem: ManifestCollectionMenuItem; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/types.ts new file mode 100644 index 0000000000..2cd985cb85 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/types.ts @@ -0,0 +1 @@ +export type * from './collection-menu-item.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/index.ts new file mode 100644 index 0000000000..e3d7b85788 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/index.ts @@ -0,0 +1,4 @@ +export * from './default/index.js'; +export * from './collection-menu-item.context.token.js'; +export * from './collection-menu-item.element.js'; +export type * from './collection-menu-item-context.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/manifests.ts new file mode 100644 index 0000000000..43d020c74e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as defaultManifests } from './default/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [...defaultManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts index b45660db04..07bfa14320 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts @@ -1,3 +1,4 @@ export * from './views/constants.js'; export const UMB_USER_COLLECTION_ALIAS = 'Umb.Collection.User'; export { UMB_USER_COLLECTION_CONTEXT } from './user-collection.context-token.js'; +export * from './menu/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts index b54121406d..59374f44b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts @@ -1,7 +1,8 @@ import { UMB_USER_COLLECTION_REPOSITORY_ALIAS } from './repository/index.js'; +import { manifests as collectionActionManifests } from './action/manifests.js'; +import { manifests as collectionMenuManifests } from './menu/manifests.js'; import { manifests as collectionRepositoryManifests } from './repository/manifests.js'; import { manifests as collectionViewManifests } from './views/manifests.js'; -import { manifests as collectionActionManifests } from './action/manifests.js'; import { UMB_USER_COLLECTION_ALIAS } from './constants.js'; export const manifests: Array = [ @@ -15,7 +16,8 @@ export const manifests: Array = [ repositoryAlias: UMB_USER_COLLECTION_REPOSITORY_ALIAS, }, }, + ...collectionActionManifests, + ...collectionMenuManifests, ...collectionRepositoryManifests, ...collectionViewManifests, - ...collectionActionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts new file mode 100644 index 0000000000..9b0faca55e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts @@ -0,0 +1 @@ +export const UMB_USER_COLLECTION_MENU_ALIAS = 'Umb.CollectionMenu.User'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts new file mode 100644 index 0000000000..4c5ede48d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_USER_ENTITY_TYPE } from '../../entity.js'; +import { UMB_USER_COLLECTION_REPOSITORY_ALIAS } from '../repository/constants.js'; +import { UMB_USER_COLLECTION_MENU_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'collectionMenu', + kind: 'default', + alias: UMB_USER_COLLECTION_MENU_ALIAS, + name: 'User Collection Menu', + meta: { + collectionRepositoryAlias: UMB_USER_COLLECTION_REPOSITORY_ALIAS, + }, + }, + { + type: 'collectionMenuItem', + kind: 'default', + alias: 'Umb.CollectionMenuItem.User', + name: 'User Collection Menu Item', + element: () => import('./user-collection-menu-item.element.js'), + forEntityTypes: [UMB_USER_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts new file mode 100644 index 0000000000..a0ec66d78a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts @@ -0,0 +1,88 @@ +import type { UmbUserDetailModel } from '../../types.js'; +import { html, customElement, css, state, property, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbCollectionMenuItemContext } from '@umbraco-cms/backoffice/collection'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('umb-user-collection-menu-item') +export class UmbUserCollectionMenuItemElement extends UmbLitElement { + @property({ type: Object, attribute: false }) + set item(newVal: UmbUserDetailModel) { + this._item = newVal; + + if (this._item) { + this.#initItem(); + } + } + get item(): UmbUserDetailModel | undefined { + return this._item; + } + protected _item?: UmbUserDetailModel; + + @property({ type: Object, attribute: false }) + public set api(value: UmbCollectionMenuItemContext | undefined) { + this.#api = value; + + if (this.#api) { + this.observe(this.#api.isSelectable, (value) => (this._isSelectable = value)); + this.observe(this.#api.isSelected, (value) => (this._isSelected = value)); + this.#initItem(); + } + } + public get api(): UmbCollectionMenuItemContext | undefined { + return this.#api; + } + #api: UmbCollectionMenuItemContext | undefined; + + @state() + protected _isActive = false; + + @state() + protected _isSelected = false; + + @state() + protected _isSelectable = false; + + #initItem() { + if (!this.#api) return; + if (!this._item) return; + this.#api.setItem(this._item); + } + + override render() { + const item = this._item; + if (!item) return nothing; + + return html` + this.#api?.select()} + @deselected=${() => this.#api?.deselect()}> + + + `; + } + + static override styles = [ + UmbTextStyles, + css` + umb-user-avatar { + font-size: 10px; + } + `, + ]; +} + +export { UmbUserCollectionMenuItemElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-collection-menu-item': UmbUserCollectionMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts index 03273e88df..ec1100d7a6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts @@ -3,7 +3,6 @@ export const manifests: Array = [ type: 'modal', alias: 'Umb.Modal.User.Picker', name: 'User Picker Modal', - js: () => import('./user-picker/user-picker-modal.element.js'), }, { type: 'modal', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts deleted file mode 100644 index e79f930e88..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { UmbUserCollectionRepository } from '../../collection/repository/user-collection.repository.js'; -import type { UmbUserItemModel } from '../../repository/item/index.js'; -import type { UmbUserPickerModalData, UmbUserPickerModalValue } from './user-picker-modal.token.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; -import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; - -@customElement('umb-user-picker-modal') -export class UmbUserPickerModalElement extends UmbModalBaseElement { - @state() - private _users: Array = []; - - #selectionManager = new UmbSelectionManager(this); - #userCollectionRepository = new UmbUserCollectionRepository(this); - - override connectedCallback(): void { - super.connectedCallback(); - - // TODO: in theory this config could change during the lifetime of the modal, so we could observe it - this.#selectionManager.setMultiple(this.data?.multiple ?? false); - this.#selectionManager.setSelection(this.value?.selection ?? []); - } - - protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void { - super.firstUpdated(_changedProperties); - this.#requestUsers(); - } - - async #requestUsers() { - if (!this.#userCollectionRepository) return; - const { data } = await this.#userCollectionRepository.requestCollection(); - - if (data) { - this._users = data.items; - } - } - - #submit() { - this.value = { selection: this.#selectionManager.getSelection() }; - this.modalContext?.submit(); - } - - #close() { - this.modalContext?.reject(); - } - - override render() { - return html` - - - ${this._users.map( - (user) => html` - this.#selectionManager.select(user.unique)} - @deselected=${() => this.#selectionManager.deselect(user.unique)} - ?selected=${this.#selectionManager.isSelected(user.unique)}> - - - `, - )} - -
- - -
-
- `; - } - - static override styles = [ - UmbTextStyles, - css` - umb-user-avatar { - font-size: 12px; - } - `, - ]; -} - -export default UmbUserPickerModalElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-user-picker-modal': UmbUserPickerModalElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts index a72fc779c8..2f2e030b6c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts @@ -1,19 +1,29 @@ import type { UmbUserDetailModel } from '../../types.js'; -import type { UmbPickerModalData } from '@umbraco-cms/backoffice/modal'; +import { UMB_USER_COLLECTION_MENU_ALIAS } from '../../collection/constants.js'; +import type { + UmbCollectionItemPickerModalData, + UmbCollectionItemPickerModalValue, +} from '@umbraco-cms/backoffice/collection'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -export type UmbUserPickerModalData = UmbPickerModalData; +export type UmbUserPickerModalData = UmbCollectionItemPickerModalData; -export interface UmbUserPickerModalValue { - selection: Array; -} +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbUserPickerModalValue extends UmbCollectionItemPickerModalValue {} export const UMB_USER_PICKER_MODAL = new UmbModalToken( - 'Umb.Modal.User.Picker', + /* TODO: use constant. We had to use the string directly here to avoid a circular dependency. + When we have removed the dataType (dependency on content) from the picker context we update this */ + 'Umb.Modal.CollectionItemPicker', { modal: { type: 'sidebar', size: 'small', }, + data: { + collection: { + menuAlias: UMB_USER_COLLECTION_MENU_ALIAS, + }, + }, }, ); From 11bf60a67c19ef51bbf699952ff905e9e249e748 Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:37:44 +0200 Subject: [PATCH 05/28] E2E: Updated exisiting acceptance tests to match updates from front end (#20525) * Updated tests * E2E: Updated acceptance tests to match changes (#20493) * Updated tests to match changes * More updates * Bumped version * Reverted change --- .../Umbraco.Tests.AcceptanceTest/package-lock.json | 8 ++++---- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../tests/DefaultConfig/Content/Content.spec.ts | 4 ++-- .../DefaultConfig/Content/ContentInfoTab.spec.ts | 2 +- .../ContentWithAllowedChildNodes.spec.ts | 2 +- .../Content/ContentWithTrueFalse.spec.ts | 2 +- .../CreateContentFromDocumentBlueprint.spec.ts | 14 +++++++------- .../DataType/BlockGrid/BlockGridEditor.spec.ts | 2 +- .../BlockListEditor/BlockListEditor.spec.ts | 2 +- .../tests/DefaultConfig/Media/Media.spec.ts | 1 + .../tests/DefaultConfig/Webhook/Webhook.spec.ts | 4 ++-- .../DefaultConfig/Webhook/WebhookTrigger.spec.ts | 2 +- 12 files changed, 23 insertions(+), 22 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index d6c157d75f..87ed32ef3a 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 49cc458287..f1a7b85fa4 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 f4b69a1231..edea7a59e4 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 54c07aa39f..a279537644 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/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts index 65437ebcac..ec7d32041d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts @@ -51,7 +51,7 @@ test('cannot create child content if allowed child node is disabled', async ({um // Assert await umbracoUi.content.isDocumentTypeNameVisible(documentTypeName, false); - await umbracoUi.content.doesModalHaveText(noAllowedDocumentTypeAvailableMessage); + await umbracoUi.content.doesDocumentModalHaveText(noAllowedDocumentTypeAvailableMessage); }); test('can create multiple child nodes with different document types', async ({umbracoApi, umbracoUi}) => { 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 75e252c28c..f58f224b6c 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/Content/CreateContentFromDocumentBlueprint.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CreateContentFromDocumentBlueprint.spec.ts index 7ec3a9223f..0b3bf06cf0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CreateContentFromDocumentBlueprint.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CreateContentFromDocumentBlueprint.spec.ts @@ -51,7 +51,7 @@ test('can create content using an invariant document blueprint', async ({umbraco await umbracoUi.content.clickActionsMenuAtRoot(); await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName); + await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName); await umbracoUi.content.clickSaveButtonForContent(); // Assert @@ -75,7 +75,7 @@ test('can create content using a variant document blueprint', async ({umbracoApi await umbracoUi.content.clickActionsMenuAtRoot(); await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName); + await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName); await umbracoUi.content.clickSaveButtonForContent(); await umbracoUi.content.clickSaveButton(); @@ -104,7 +104,7 @@ test('can create content with different name using an invariant document bluepri await umbracoUi.content.clickActionsMenuAtRoot(); await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName); + await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButtonForContent(); @@ -130,7 +130,7 @@ test('can create content with different name using a variant document blueprint' await umbracoUi.content.clickActionsMenuAtRoot(); await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName); + await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName); await umbracoUi.content.enterContentName(contentName); await umbracoUi.content.clickSaveButtonForContent(); await umbracoUi.content.clickSaveButton(); @@ -161,7 +161,7 @@ test('can create content using a document blueprint with block list', async ({um await umbracoUi.content.clickActionsMenuAtRoot(); await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName); + await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName); await umbracoUi.content.clickSaveButtonForContent(); // Assert @@ -187,7 +187,7 @@ test('can create content using a document blueprint with block grid', async ({um await umbracoUi.content.clickActionsMenuAtRoot(); await umbracoUi.content.clickCreateActionMenuOption(); await umbracoUi.content.chooseDocumentType(documentTypeName); - await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName); + await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName); await umbracoUi.content.clickSaveButtonForContent(); // Assert @@ -197,4 +197,4 @@ test('can create content using a document blueprint with block grid', async ({um expect(contentData.values[0].value.contentData[0].values[0].value.markup).toEqual(textContent); const blockListValue = contentData.values.find(item => item.editorAlias === "Umbraco.BlockGrid")?.value; expect(blockListValue).toBeTruthy(); -}); \ No newline at end of file +}); 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 54eed6a464..e2b5d799ec 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 1239425d20..5db02e7d48 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 ab749dc20a..e70132f3c5 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); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts index 650c7d09d6..51544486d5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts @@ -173,7 +173,7 @@ test('can remove a header from a webhook', async ({umbracoApi, umbracoUi}) => { expect(await umbracoApi.webhook.doesWebhookHaveHeader(webhookName, headerName, headerValue)).toBeFalsy(); }); -test('cannot add both content event and media event for a webhook', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { +test('cannot add both content event and media event for a webhook', async ({umbracoApi, umbracoUi}) => { // Arrange const event = 'Content Published'; await umbracoApi.webhook.createDefaultWebhook(webhookName, webhookSiteToken, event); @@ -185,4 +185,4 @@ test('cannot add both content event and media event for a webhook', {tag: '@rele // Assert await umbracoUi.webhook.isModalMenuItemWithNameDisabled('Media Saved'); await umbracoUi.webhook.isModalMenuItemWithNameDisabled('Media Deleted'); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/WebhookTrigger.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/WebhookTrigger.spec.ts index 51dc9ede2d..9d4f1c68a2 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/WebhookTrigger.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/WebhookTrigger.spec.ts @@ -27,7 +27,7 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.media.ensureNameNotExists(mediaName); }); -test('can trigger when content is published', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => { +test('can trigger when content is published', async ({umbracoApi, umbracoUi}) => { test.slow(); // Arrange From 7d0adc4755c767b052df0f064828792c65a82b19 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 06/28] build(deps): bumps @umbraco-ui/uui from 1.16.0-rc.0 to 1.16.0 --- src/Umbraco.Web.UI.Client/package-lock.json | 966 +++++++++--------- .../src/external/uui/package.json | 4 +- 2 files changed, 485 insertions(+), 485 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index c7805ee9ac..2e8f7a23db 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -3719,909 +3719,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==", + "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-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" + "@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": { @@ -16988,8 +16988,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 6bc495b293..a871f79a8e 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 65798b572281d4df6d5bbc4dd5fda0d23623c1e1 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 17 Oct 2025 10:46:54 +0200 Subject: [PATCH 07/28] 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 a81f4c0f54..a40e9c4b06 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 35ff79bb43..0ea8f97d8c 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 f82c0a90e7a235189a88c5f92dfb0512c79bec4a Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Tue, 14 Oct 2025 07:46:48 +0100 Subject: [PATCH 08/28] UFM: Adds `$index` support to Block editors (fixes #20470) (#20488) * Block List: adds `$index` support for UFM labels * Block Grid: adds `$index` support for UFM labels * Block RTE: adds `$index` support for UFM labels Which is always zero `0`. But has been wired up if we do implement the index order in future. --- .../block-grid-block-inline.element.ts | 5 ++++- .../block-grid-block.element.ts | 5 ++++- .../block-grid-entries.element.ts | 8 ++++---- .../block-grid-entry.element.ts | 4 +++- .../block-list-entry.element.ts | 17 +++++++++-------- .../inline-list-block.element.ts | 5 ++++- .../ref-list-block/ref-list-block.element.ts | 5 ++++- .../property-editor-ui-block-list.element.ts | 11 ++++++----- .../block-rte-entry/block-rte-entry.element.ts | 11 ++++++----- .../ref-rte-block/ref-rte-block.element.ts | 5 ++++- 10 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts index f1dc585342..6a515902f1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts @@ -38,6 +38,9 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { @property({ type: String, reflect: false }) icon?: string; + @property({ type: Number, attribute: false }) + index?: number; + @property({ type: Boolean, reflect: true }) unpublished?: boolean; @@ -173,7 +176,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement { } #renderBlockInfo() { - 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-grid/components/block-grid-block/block-grid-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts index c6406db6bf..92e27823e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts @@ -14,6 +14,9 @@ export class UmbBlockGridBlockElement extends UmbLitElement { @property({ type: String, reflect: false }) icon?: string; + @property({ type: Number, attribute: false }) + index?: number; + @property({ attribute: false }) config?: UmbBlockEditorCustomViewConfiguration; @@ -27,7 +30,7 @@ export class UmbBlockGridBlockElement extends UmbLitElement { settings?: UmbBlockDataType; override render() { - const blockValue = { ...this.content, $settings: this.settings }; + const blockValue = { ...this.content, $settings: this.settings, $index: this.index }; return html` ${repeat( this._layoutEntries, - (x) => x.contentKey, - (layoutEntry, index) => + (layout, index) => `${index}_${layout.contentKey}`, + (layout, index) => html` + .contentKey=${layout.contentKey} + .layout=${layout}> `, )} 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 5fb10c3349..ac93c62bff 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 5e3a68e444..554dfd36b5 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 1be1ace010..574f72143c 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 246256e629..76b7e263ba 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 @@ -378,12 +378,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)} `, @@ -396,7 +397,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 63d79ac8f8..e9624ed2eb 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 7e7d9da144f2aec1821d3bfce2105b73a3b5c6a3 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 09/28] 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 6d7539b825..f565b9cdf0 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 class DocumentVariantStateHelper culture, content.Edited, content.Published, + content.Trashed, content.AvailableCultures, content.EditedCultures ?? Enumerable.Empty(), content.PublishedCultures); @@ -22,17 +23,23 @@ internal static class DocumentVariantStateHelper 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 3ed51114e1..d3edd54cd9 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 d63eda2eed..f3c0fe9f70 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 class DocumentVariantStateHelperTests 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 class DocumentVariantStateHelperTests && 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 class DocumentVariantStateHelperTests 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 class DocumentVariantStateHelperTests 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 class DocumentVariantStateHelperTests && 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 72d44decd475ef0fbb0564e7209809c4f19c3e22 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 17 Oct 2025 15:30:43 +0200 Subject: [PATCH 10/28] 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 5f13509f56..51a57abdbd 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 0bd3f90ea5..9ea9d69e56 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 3622652b51..531bca043e 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 45ef5f33bc..5be90a4278 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 92dbaf71629367b529ad59b9d6259d177b596966 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 17 Oct 2025 19:49:05 +0200 Subject: [PATCH 11/28] Document/Media: Readonly when in recycle bin (#20541) * make document and media readonly when trashed + reload the entity * introduce restore event + remove readonly * handle media audit log todos * disable content type picker when trashed * disable template picker when trashed --- .../core/recycle-bin/entity-action/index.ts | 5 +- .../restore-from-recycle-bin/index.ts | 1 + .../restore-from-recycle-bin.action.ts | 8 ++ .../restore-from-recycle-bin.event.ts | 9 +++ .../workspace/document-workspace.context.ts | 75 +++++++++++++++--- .../document-workspace-view-info.element.ts | 30 ++++++-- .../media/media/audit-log/info-app/utils.ts | 30 ++++++++ .../workspace/media-workspace.context.ts | 76 ++++++++++++++++--- .../info/media-workspace-view-info.element.ts | 16 +++- 9 files changed, 218 insertions(+), 32 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/restore-from-recycle-bin.event.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/index.ts index 9e7d4fe545..bd395ad28f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/index.ts @@ -1,3 +1,6 @@ export { UmbTrashEntityAction, UmbEntityTrashedEvent } from './trash/index.js'; -export { UmbRestoreFromRecycleBinEntityAction } from './restore-from-recycle-bin/restore-from-recycle-bin.action.js'; +export { + UmbRestoreFromRecycleBinEntityAction, + UmbEntityRestoredFromRecycleBinEvent, +} from './restore-from-recycle-bin/index.js'; export { UmbEmptyRecycleBinEntityAction } from './empty-recycle-bin/empty-recycle-bin.action.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/index.ts index 98805cb678..171b7ec3ed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/index.ts @@ -1,2 +1,3 @@ export * from './restore-from-recycle-bin.action.js'; +export * from './restore-from-recycle-bin.event.js'; export type * from './types.js'; 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 cd8e4ac1b2..63dc760e12 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,5 +1,6 @@ import { UMB_RESTORE_FROM_RECYCLE_BIN_MODAL } from './modal/restore-from-recycle-bin-modal.token.js'; import type { MetaEntityActionRestoreFromRecycleBinKind } from './types.js'; +import { UmbEntityRestoredFromRecycleBinEvent } from './restore-from-recycle-bin.event.js'; import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; @@ -40,6 +41,13 @@ export class UmbRestoreFromRecycleBinEntityAction extends UmbEntityActionBase { + this.#removeEventListeners(); + this.#actionEventContext = actionEventContext; + this.#addEventListeners(); + }); + this.observe( this.contentTypeUnique, (unique) => { @@ -139,6 +151,8 @@ export class UmbDocumentWorkspaceContext null, ); + this.observe(this.isTrashed, (isTrashed) => this.#onTrashStateChange(isTrashed)); + this.routes.setRoutes([ { path: UMB_CREATE_FROM_BLUEPRINT_DOCUMENT_WORKSPACE_PATH_PATTERN.toString(), @@ -211,16 +225,6 @@ export class UmbDocumentWorkspaceContext this.#isTrashedContext.setIsTrashed(false); } - override async load(unique: string) { - const response = await super.load(unique); - - if (response?.data) { - this.#isTrashedContext.setIsTrashed(response.data.isTrashed); - } - - return response; - } - protected override async loadSegments(): Promise { this.observe( this.unique, @@ -401,6 +405,53 @@ export class UmbDocumentWorkspaceContext }, ); } + + #addEventListeners() { + this.#actionEventContext?.addEventListener(UmbEntityTrashedEvent.TYPE, this.#onRecycleBinEvent as EventListener); + this.#actionEventContext?.addEventListener( + UmbEntityRestoredFromRecycleBinEvent.TYPE, + this.#onRecycleBinEvent as EventListener, + ); + } + + #removeEventListeners() { + this.#actionEventContext?.removeEventListener(UmbEntityTrashedEvent.TYPE, this.#onRecycleBinEvent as EventListener); + this.#actionEventContext?.removeEventListener( + UmbEntityRestoredFromRecycleBinEvent.TYPE, + this.#onRecycleBinEvent as EventListener, + ); + } + + #onRecycleBinEvent = (event: UmbEntityTrashedEvent | UmbEntityRestoredFromRecycleBinEvent) => { + const unique = this.getUnique(); + const entityType = this.getEntityType(); + if (event.getUnique() !== unique || event.getEntityType() !== entityType) return; + this.reload(); + }; + + #onTrashStateChange(isTrashed?: boolean) { + this.#isTrashedContext.setIsTrashed(isTrashed ?? false); + + const guardUnique = `UMB_PREVENT_EDIT_TRASHED_ITEM`; + + if (!isTrashed) { + this.readOnlyGuard.removeRule(guardUnique); + return; + } + + const rule: UmbVariantGuardRule = { + unique: guardUnique, + permitted: true, + }; + + // TODO: Change to use property write guard when it supports making the name read-only. + this.readOnlyGuard.addRule(rule); + } + + public override destroy(): void { + this.#removeEventListeners(); + super.destroy(); + } } export default UmbDocumentWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts index 129c64ea77..d703ded970 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts @@ -14,6 +14,7 @@ import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; +import { UMB_IS_TRASHED_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/recycle-bin'; @customElement('umb-document-workspace-view-info') export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { @@ -49,6 +50,9 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { @state() private _hasSettingsAccess: boolean = false; + @state() + private _isTrashed: boolean = false; + #workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE; #templateRepository = new UmbTemplateItemRepository(this); #documentPublishingWorkspaceContext?: typeof UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT.TYPE; @@ -85,6 +89,16 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { this.#observePendingChanges(); }); + this.consumeContext(UMB_IS_TRASHED_ENTITY_CONTEXT, (context) => { + this.observe( + context?.isTrashed, + (isTrashed) => { + this._isTrashed = isTrashed ?? false; + }, + '_isTrashed', + ); + }); + createExtensionApiByAlias(this, UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS, [ { config: { @@ -202,7 +216,7 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { href=${ifDefined( this._hasSettingsAccess ? editDocumentTypePath + 'edit/' + this._documentTypeUnique : undefined, )} - ?readonly=${!this._hasSettingsAccess} + ?readonly=${!this._hasSettingsAccess || this._isTrashed} name=${ifDefined(this.localize.string(this._documentTypeName ?? ''))}> @@ -231,13 +245,15 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { href=${ifDefined( this._hasSettingsAccess ? editTemplatePath + 'edit/' + this._templateUnique : undefined, )} - ?readonly=${!this._hasSettingsAccess}> + ?readonly=${!this._hasSettingsAccess || this._isTrashed}> - - - + ${!this._isTrashed + ? html` + + ` + : nothing} ` : html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/utils.ts index e410f350d4..773169de4a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/audit-log/info-app/utils.ts @@ -30,6 +30,36 @@ export function getMediaHistoryTagStyleAndText(type: UmbMediaAuditLogType): Hist text: { label: 'auditTrails_smallSave', desc: 'auditTrails_save' }, }; + case UmbMediaAuditLog.COPY: + return { + style: { look: 'secondary', color: 'default' }, + text: { label: 'auditTrails_smallCopy', desc: 'auditTrails_copy' }, + }; + + case UmbMediaAuditLog.MOVE: + return { + style: { look: 'secondary', color: 'default' }, + text: { label: 'auditTrails_smallMove', desc: 'auditTrails_move' }, + }; + + case UmbMediaAuditLog.DELETE: + return { + style: { look: 'secondary', color: 'danger' }, + text: { label: 'auditTrails_smallDelete', desc: 'auditTrails_delete' }, + }; + + case UmbMediaAuditLog.SORT: + return { + style: { look: 'secondary', color: 'default' }, + text: { label: 'auditTrails_smallSort', desc: 'auditTrails_sort' }, + }; + + case UmbMediaAuditLog.CUSTOM: + return { + style: { look: 'placeholder', color: 'default' }, + text: { label: 'auditTrails_smallCustom', desc: 'auditTrails_custom' }, + }; + default: return { style: { look: 'placeholder', color: 'danger' }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index 4668315e9b..495c121ad8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -9,7 +9,11 @@ import { UMB_MEDIA_COLLECTION_ALIAS } from '../collection/constants.js'; import type { UmbMediaDetailRepository } from '../repository/index.js'; import { UMB_MEDIA_WORKSPACE_ALIAS, UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD } from './constants.js'; import { UmbContentDetailWorkspaceContextBase, type UmbContentWorkspaceContext } from '@umbraco-cms/backoffice/content'; -import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; +import { + UmbEntityRestoredFromRecycleBinEvent, + UmbEntityTrashedEvent, + UmbIsTrashedEntityContext, +} from '@umbraco-cms/backoffice/recycle-bin'; import { UmbWorkspaceIsNewRedirectController, UmbWorkspaceIsNewRedirectControllerAlias, @@ -17,6 +21,8 @@ import { import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbMediaTypeDetailModel } from '@umbraco-cms/backoffice/media-type'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantGuardRule } from '@umbraco-cms/backoffice/utils'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; type ContentModel = UmbMediaDetailModel; type ContentTypeModel = UmbMediaTypeDetailModel; @@ -30,6 +36,8 @@ export class UmbMediaWorkspaceContext > implements UmbContentWorkspaceContext { + readonly isTrashed = this._data.createObservablePartOfCurrent((data) => data?.isTrashed); + readonly contentTypeUnique = this._data.createObservablePartOfCurrent((data) => data?.mediaType.unique); /* * @deprecated Use `collection.hasCollection` instead, will be removed in v.18 @@ -38,6 +46,7 @@ export class UmbMediaWorkspaceContext readonly contentTypeIcon = this._data.createObservablePartOfCurrent((data) => data?.mediaType.icon); #isTrashedContext = new UmbIsTrashedEntityContext(this); + #actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; constructor(host: UmbControllerHost) { super(host, { @@ -61,6 +70,14 @@ export class UmbMediaWorkspaceContext null, ); + this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (actionEventContext) => { + this.#removeEventListeners(); + this.#actionEventContext = actionEventContext; + this.#addEventListeners(); + }); + + this.observe(this.isTrashed, (isTrashed) => this.#onTrashStateChange(isTrashed)); + this.propertyViewGuard.fallbackToPermitted(); this.propertyWriteGuard.fallbackToPermitted(); @@ -102,16 +119,6 @@ export class UmbMediaWorkspaceContext this.removeUmbControllerByAlias(UmbWorkspaceIsNewRedirectControllerAlias); } - public override async load(unique: string) { - const response = await super.load(unique); - - if (response?.data) { - this.#isTrashedContext.setIsTrashed(response.data.isTrashed); - } - - return response; - } - /* * @deprecated Use `createScaffold` instead. */ @@ -154,6 +161,53 @@ export class UmbMediaWorkspaceContext ): UmbMediaPropertyDatasetContext { return new UmbMediaPropertyDatasetContext(host, this, variantId); } + + #addEventListeners() { + this.#actionEventContext?.addEventListener(UmbEntityTrashedEvent.TYPE, this.#onRecycleBinEvent as EventListener); + this.#actionEventContext?.addEventListener( + UmbEntityRestoredFromRecycleBinEvent.TYPE, + this.#onRecycleBinEvent as EventListener, + ); + } + + #removeEventListeners() { + this.#actionEventContext?.removeEventListener(UmbEntityTrashedEvent.TYPE, this.#onRecycleBinEvent as EventListener); + this.#actionEventContext?.removeEventListener( + UmbEntityRestoredFromRecycleBinEvent.TYPE, + this.#onRecycleBinEvent as EventListener, + ); + } + + #onRecycleBinEvent = (event: UmbEntityTrashedEvent | UmbEntityRestoredFromRecycleBinEvent) => { + const unique = this.getUnique(); + const entityType = this.getEntityType(); + if (event.getUnique() !== unique || event.getEntityType() !== entityType) return; + this.reload(); + }; + + #onTrashStateChange(isTrashed?: boolean) { + this.#isTrashedContext.setIsTrashed(isTrashed ?? false); + + const guardUnique = `UMB_PREVENT_EDIT_TRASHED_ITEM`; + + if (!isTrashed) { + this.readOnlyGuard.removeRule(guardUnique); + return; + } + + const rule: UmbVariantGuardRule = { + unique: guardUnique, + permitted: true, + }; + + // TODO: Change to use property write guard when it supports making the name read-only. + this.readOnlyGuard.addRule(rule); + } + + public override destroy(): void { + this.#removeEventListeners(); + super.destroy(); + } } export { UmbMediaWorkspaceContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts index 0dd30bbea2..77fbe7839d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts @@ -10,6 +10,7 @@ import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; +import { UMB_IS_TRASHED_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/recycle-bin'; @customElement('umb-media-workspace-view-info') export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { @@ -41,6 +42,9 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { @state() private _hasSettingsAccess: boolean = false; + @state() + private _isTrashed: boolean = false; + constructor() { super(); @@ -70,6 +74,16 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { this.#getData(); this.#observeContent(); }); + + this.consumeContext(UMB_IS_TRASHED_ENTITY_CONTEXT, (context) => { + this.observe( + context?.isTrashed, + (isTrashed) => { + this._isTrashed = isTrashed ?? false; + }, + '_isTrashed', + ); + }); } async #getData() { @@ -123,7 +137,7 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { href=${ifDefined( this._hasSettingsAccess ? this._editMediaTypePath + 'edit/' + this._mediaTypeUnique : undefined, )} - ?readonly=${!this._hasSettingsAccess} + ?readonly=${!this._hasSettingsAccess || this._isTrashed} name=${ifDefined(this._mediaTypeName)}> ${this._mediaTypeIcon ? html`` : nothing} From a808e3ab463a9eb1c082b9e4595daea347bb29c5 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 20 Oct 2025 11:54:26 +0200 Subject: [PATCH 12/28] Added direct dependency to secure version of Microsoft.Build.Tasks.Core in Umbraco.Web.UI (#20546) Added direct dependency to secure version of Microsoft.Build.Tasks.Core in Umbraco.Web.UI. --- 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 051ecc5cfd..2ade2ca396 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -26,6 +26,11 @@ + + From ab4be79da02546f10185cfa9f0e8ce433a011d6a Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Mon, 20 Oct 2025 10:51:38 +0100 Subject: [PATCH 13/28] 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 (cherry picked from commit d5a2f0572edf9a842c05247cde3a8f9b6285c0e8) --- .../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 95e907a38a..26af65490b 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 aba28768e5..4a77454df8 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 21899e41f8..75de96a38c 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 94692cccb71c6f08b469d4b616d9ccdd10f979d3 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 20 Oct 2025 13:43:10 +0200 Subject: [PATCH 14/28] Document/Media Recycle Bin: Show full breadcrumb (#20547) * register structure context for recycle bin * Update manifests.ts * export consts * move href construction to context + override for document and media --- ...t-structure-workspace-context.interface.ts | 1 + ...t-tree-structure-workspace-context-base.ts | 38 ++++++++++++++- ...rkspace-variant-menu-breadcrumb.element.ts | 25 +++------- .../menu/document-menu-structure.context.ts | 14 +++++- .../documents/documents/menu/manifests.ts | 5 +- .../documents/recycle-bin/constants.ts | 1 + .../documents/recycle-bin/manifests.ts | 4 +- .../recycle-bin/menu-item/manifests.ts | 23 ---------- .../documents/recycle-bin/menu/constants.ts | 1 + ...ment-recycle-bin-menu-structure.context.ts | 11 +++++ .../documents/recycle-bin/menu/manifests.ts | 46 +++++++++++++++++++ .../packages/media/media/menu/manifests.ts | 6 +-- .../menu/media-menu-structure.context.ts | 14 +++++- .../media/media/recycle-bin/constants.ts | 3 +- .../media/media/recycle-bin/manifests.ts | 4 +- .../media/recycle-bin/menu-item/manifests.ts | 22 --------- .../media/media/recycle-bin/menu/constants.ts | 1 + .../media/media/recycle-bin/menu/manifests.ts | 44 ++++++++++++++++++ ...edia-recycle-bin-menu-structure.context.ts | 11 +++++ 19 files changed, 197 insertions(+), 77 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu-item/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/document-recycle-bin-menu-structure.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/manifests.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu-item/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/media-recycle-bin-menu-structure.context.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.interface.ts index ade079e3cc..bea694e43b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-structure-workspace-context.interface.ts @@ -4,4 +4,5 @@ import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; export interface UmbMenuVariantStructureWorkspaceContext extends UmbContext { structure: Observable; + getItemHref(structureItem: UmbVariantStructureItemModel): string | undefined; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts index 5cd6290474..db57dba46c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts @@ -7,9 +7,14 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbAncestorsEntityContext, UmbParentEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity'; -import { UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { + UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT, + UMB_VARIANT_WORKSPACE_CONTEXT, +} from '@umbraco-cms/backoffice/workspace'; import { linkEntityExpansionEntries } from '@umbraco-cms/backoffice/utils'; import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; interface UmbMenuVariantTreeStructureWorkspaceContextBaseArgs { treeRepositoryAlias: string; @@ -31,11 +36,15 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um */ public readonly parent = this.#parent.asObservable(); + protected _sectionContext?: typeof UMB_SECTION_CONTEXT.TYPE; + #parentContext = new UmbParentEntityContext(this); #ancestorContext = new UmbAncestorsEntityContext(this); #sectionSidebarMenuContext?: typeof UMB_SECTION_SIDEBAR_MENU_SECTION_CONTEXT.TYPE; #isModalContext: boolean = false; #isNew: boolean | undefined = undefined; + #variantWorkspaceContext?: typeof UMB_VARIANT_WORKSPACE_CONTEXT.TYPE; + #workspaceActiveVariantId?: UmbVariantId; public readonly IS_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT = true; @@ -49,6 +58,16 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um this.#isModalContext = modalContext !== undefined; }); + this.consumeContext(UMB_SECTION_CONTEXT, (instance) => { + this._sectionContext = instance; + }); + + this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (instance) => { + if (!instance) return; + this.#variantWorkspaceContext = instance; + this.#observeWorkspaceActiveVariant(); + }); + this.consumeContext(UMB_SECTION_SIDEBAR_MENU_SECTION_CONTEXT, (instance) => { this.#sectionSidebarMenuContext = instance; }); @@ -75,6 +94,10 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um }); } + getItemHref(structureItem: UmbVariantStructureItemModel): string | undefined { + return `section/${this._sectionContext?.getPathname()}/workspace/${structureItem.entityType}/edit/${structureItem.unique}/${this.#workspaceActiveVariantId?.toCultureString()}`; + } + async #requestStructure() { const isNew = this.#workspaceContext?.getIsNew(); const uniqueObservable = isNew @@ -191,6 +214,19 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um this.#sectionSidebarMenuContext?.expansion.expandItems(expandableItemsWithMenuItem); } + #observeWorkspaceActiveVariant() { + this.observe( + this.#variantWorkspaceContext?.splitView.activeVariantsInfo, + (value) => { + if (!value) return; + if (value?.length === 0) return; + this.#workspaceActiveVariantId = UmbVariantId.Create(value[0]); + }, + + 'breadcrumbWorkspaceActiveVariantObserver', + ); + } + override destroy(): void { super.destroy(); this.#structure.destroy(); 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 04fe73d6a8..b11b0459c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts @@ -6,7 +6,6 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language'; import { UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/menu'; -import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section'; import type { UmbAppLanguageContext } from '@umbraco-cms/backoffice/language'; import type { UmbVariantStructureItemModel } from '@umbraco-cms/backoffice/menu'; @@ -24,10 +23,9 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { @state() private _appDefaultCulture?: string; - #sectionContext?: typeof UMB_SECTION_CONTEXT.TYPE; #workspaceContext?: UmbVariantDatasetWorkspaceContext; #appLanguageContext?: UmbAppLanguageContext; - #structureContext?: typeof UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT.TYPE; + #menuStructureContext?: typeof UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT.TYPE; constructor() { super(); @@ -37,10 +35,6 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { this.#observeDefaultCulture(); }); - this.consumeContext(UMB_SECTION_CONTEXT, (instance) => { - this.#sectionContext = instance; - }); - this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (instance) => { if (!instance) return; this.#workspaceContext = instance; @@ -50,15 +44,15 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { this.consumeContext(UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT, (instance) => { if (!instance) return; - this.#structureContext = instance; + this.#menuStructureContext = instance; this.#observeStructure(); }); } #observeStructure() { - if (!this.#structureContext || !this.#workspaceContext) return; + if (!this.#menuStructureContext || !this.#workspaceContext) return; - this.observe(this.#structureContext.structure, (value) => { + this.observe(this.#menuStructureContext.structure, (value) => { if (!this.#workspaceContext) return; const unique = this.#workspaceContext.getUnique(); // exclude the current unique from the structure. We append this with an observer of the name @@ -113,15 +107,8 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement { return structureItem.variants?.[0]?.name ?? '(#general_unknown)'; } - #getHref(structureItem: any) { - if (structureItem.isFolder) return undefined; - - let href = `section/${this.#sectionContext?.getPathname()}`; - if (structureItem.unique) { - href += `/workspace/${structureItem.entityType}/edit/${structureItem.unique}/${this._workspaceActiveVariantId?.toCultureString()}`; - } - - return href; + #getHref(structureItem: UmbVariantStructureItemModel) { + return this.#menuStructureContext?.getItemHref(structureItem); } override render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/document-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/document-menu-structure.context.ts index 55caf0e070..6076367a37 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/document-menu-structure.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/document-menu-structure.context.ts @@ -1,11 +1,23 @@ import { UMB_DOCUMENT_TREE_REPOSITORY_ALIAS } from '../tree/index.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; +import { + UmbMenuVariantTreeStructureWorkspaceContextBase, + type UmbVariantStructureItemModel, +} from '@umbraco-cms/backoffice/menu'; export class UmbDocumentMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase { constructor(host: UmbControllerHost) { super(host, { treeRepositoryAlias: UMB_DOCUMENT_TREE_REPOSITORY_ALIAS }); } + + override getItemHref(structureItem: UmbVariantStructureItemModel): string | undefined { + // The Document menu does not have a root item, so we do not have a href for it. + if (!structureItem.unique) { + return `section/${this._sectionContext?.getPathname()}`; + } else { + return super.getItemHref(structureItem); + } + } } export default UmbDocumentMenuStructureContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/manifests.ts index 50309def83..d12af0177c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/menu/manifests.ts @@ -1,3 +1,4 @@ +import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../constants.js'; import { UMB_DOCUMENT_TREE_ALIAS } from '../tree/index.js'; import { UMB_DOCUMENT_MENU_ITEM_ALIAS } from './constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; @@ -36,7 +37,7 @@ export const manifests: Array = [ conditions: [ { alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: 'Umb.Workspace.Document', + match: UMB_DOCUMENT_WORKSPACE_ALIAS, }, { alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, @@ -51,7 +52,7 @@ export const manifests: Array = [ conditions: [ { alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: 'Umb.Workspace.Document', + match: UMB_DOCUMENT_WORKSPACE_ALIAS, }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts index 0002782c2b..82db473915 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts @@ -1,3 +1,4 @@ +export * from './menu/constants.js'; export * from './repository/constants.js'; export * from './tree/constants.js'; export const UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'document-recycle-bin-root'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/manifests.ts index cc3ebf859f..55c305be6e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/manifests.ts @@ -1,5 +1,5 @@ import { manifests as entityActionManifests } from './entity-action/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; @@ -11,7 +11,7 @@ export const manifests: Array = [ api: () => import('./allow-document-recycle-bin.condition.js'), }, ...entityActionManifests, - ...menuItemManifests, + ...menuManifests, ...repositoryManifests, ...treeManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu-item/manifests.ts deleted file mode 100644 index 65fca591fd..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu-item/manifests.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { UMB_CONTENT_MENU_ALIAS } from '../../menu/manifests.js'; -import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS } from '../constants.js'; - -export const manifests: Array = [ - { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.Document.RecycleBin', - name: 'Document Recycle Bin Menu Item', - weight: 100, - meta: { - treeAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS, - label: 'Recycle Bin', - icon: 'icon-trash', - menus: [UMB_CONTENT_MENU_ALIAS], - }, - conditions: [ - { - alias: 'Umb.Condition.CurrentUser.AllowDocumentRecycleBin', - }, - ], - }, -]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/constants.ts new file mode 100644 index 0000000000..b7af8eaecb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/constants.ts @@ -0,0 +1 @@ +export const UMB_DOCUMENT_RECYCLE_BIN_MENU_ITEM_ALIAS = 'Umb.MenuItem.Document.RecycleBin'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/document-recycle-bin-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/document-recycle-bin-menu-structure.context.ts new file mode 100644 index 0000000000..94a68d442f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/document-recycle-bin-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_DOCUMENT_RECYCLE_BIN_TREE_REPOSITORY_ALIAS } from '../tree/constants.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +export class UmbDocumentRecycleBinMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_REPOSITORY_ALIAS }); + } +} + +export { UmbDocumentRecycleBinMenuStructureContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/manifests.ts new file mode 100644 index 0000000000..9b53239f0f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/menu/manifests.ts @@ -0,0 +1,46 @@ +import { UMB_CONTENT_MENU_ALIAS } from '../../menu/manifests.js'; +import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS } from '../constants.js'; +import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../../constants.js'; +import { UMB_DOCUMENT_RECYCLE_BIN_MENU_ITEM_ALIAS } from './constants.js'; +import { UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'menuItem', + kind: 'tree', + alias: UMB_DOCUMENT_RECYCLE_BIN_MENU_ITEM_ALIAS, + name: 'Document Recycle Bin Menu Item', + weight: 100, + meta: { + treeAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS, + label: 'Recycle Bin', + icon: 'icon-trash', + menus: [UMB_CONTENT_MENU_ALIAS], + }, + conditions: [ + { + alias: 'Umb.Condition.CurrentUser.AllowDocumentRecycleBin', + }, + ], + }, + { + type: 'workspaceContext', + kind: 'menuStructure', + name: 'Document Recycle Bin Menu Structure Workspace Context', + alias: 'Umb.Context.DocumentRecycleBin.Menu.Structure', + api: () => import('./document-recycle-bin-menu-structure.context.js'), + meta: { + menuItemAlias: UMB_DOCUMENT_RECYCLE_BIN_MENU_ITEM_ALIAS, + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_WORKSPACE_ALIAS, + }, + { + alias: UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/manifests.ts index 2d5ae3ad50..34de09b5b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_MEDIA_TREE_ALIAS } from '../constants.js'; +import { UMB_MEDIA_TREE_ALIAS, UMB_MEDIA_WORKSPACE_ALIAS } from '../constants.js'; import { UMB_MEDIA_MENU_ALIAS, UMB_MEDIA_MENU_ITEM_ALIAS } from './constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; @@ -34,7 +34,7 @@ export const manifests: Array = [ conditions: [ { alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: 'Umb.Workspace.Media', + match: UMB_MEDIA_WORKSPACE_ALIAS, }, { alias: UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, @@ -49,7 +49,7 @@ export const manifests: Array = [ conditions: [ { alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: 'Umb.Workspace.Media', + match: UMB_MEDIA_WORKSPACE_ALIAS, }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/media-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/media-menu-structure.context.ts index 156b745b3c..8fe321e470 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/media-menu-structure.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/menu/media-menu-structure.context.ts @@ -1,11 +1,23 @@ import { UMB_MEDIA_TREE_REPOSITORY_ALIAS } from '../constants.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; +import { + UmbMenuVariantTreeStructureWorkspaceContextBase, + type UmbVariantStructureItemModel, +} from '@umbraco-cms/backoffice/menu'; export class UmbMediaMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase { constructor(host: UmbControllerHost) { super(host, { treeRepositoryAlias: UMB_MEDIA_TREE_REPOSITORY_ALIAS }); } + + override getItemHref(structureItem: UmbVariantStructureItemModel): string | undefined { + // The Media menu does not have a root item, so we do not have a href for it. + if (!structureItem.unique) { + return `section/${this._sectionContext?.getPathname()}`; + } else { + return super.getItemHref(structureItem); + } + } } export default UmbMediaMenuStructureContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts index e05617eb5a..49ba8c8836 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts @@ -1,3 +1,4 @@ -export * from './tree/constants.js'; +export * from './menu/constants.js'; export * from './repository/constants.js'; +export * from './tree/constants.js'; export const UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'media-recycle-bin-root'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts index 1765a1dd7b..5f87f5fcf4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts @@ -1,5 +1,5 @@ import { manifests as entityActionManifests } from './entity-action/manifests.js'; -import { manifests as menuItemManifests } from './menu-item/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; @@ -11,7 +11,7 @@ export const manifests: Array = [ api: () => import('./allow-media-recycle-bin.condition.js'), }, ...entityActionManifests, - ...menuItemManifests, + ...menuManifests, ...repositoryManifests, ...treeManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu-item/manifests.ts deleted file mode 100644 index fdabbc6855..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu-item/manifests.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { UMB_MEDIA_MENU_ALIAS, UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS } from '../../constants.js'; - -export const manifests: Array = [ - { - type: 'menuItem', - kind: 'tree', - alias: 'Umb.MenuItem.Media.RecycleBin', - name: 'Media Recycle Bin Menu Item', - weight: 100, - meta: { - treeAlias: UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS, - label: 'Recycle Bin', - icon: 'icon-trash', - menus: [UMB_MEDIA_MENU_ALIAS], - }, - conditions: [ - { - alias: 'Umb.Condition.CurrentUser.AllowMediaRecycleBin', - }, - ], - }, -]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/constants.ts new file mode 100644 index 0000000000..2224ce4c3b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/constants.ts @@ -0,0 +1 @@ +export const UMB_MEDIA_RECYCLE_BIN_MENU_ITEM_ALIAS = 'Umb.MenuItem.Media.RecycleBin'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/manifests.ts new file mode 100644 index 0000000000..fb7a75f6e9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/manifests.ts @@ -0,0 +1,44 @@ +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; +import { UMB_MEDIA_MENU_ALIAS, UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS, UMB_MEDIA_WORKSPACE_ALIAS } from '../../constants.js'; +import { UMB_MEDIA_RECYCLE_BIN_MENU_ITEM_ALIAS } from './constants.js'; +import { UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; + +export const manifests: Array = [ + { + type: 'menuItem', + kind: 'tree', + alias: UMB_MEDIA_RECYCLE_BIN_MENU_ITEM_ALIAS, + name: 'Media Recycle Bin Menu Item', + weight: 100, + meta: { + treeAlias: UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS, + label: 'Recycle Bin', + icon: 'icon-trash', + menus: [UMB_MEDIA_MENU_ALIAS], + }, + conditions: [ + { + alias: 'Umb.Condition.CurrentUser.AllowMediaRecycleBin', + }, + ], + }, + { + type: 'workspaceContext', + kind: 'menuStructure', + name: 'Media Recycle Bin Menu Structure Workspace Context', + alias: 'Umb.Context.MediaRecycleBin.Menu.Structure', + api: () => import('./media-recycle-bin-menu-structure.context.js'), + meta: { + menuItemAlias: UMB_MEDIA_RECYCLE_BIN_MENU_ITEM_ALIAS, + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_MEDIA_WORKSPACE_ALIAS, + }, + { + alias: UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/media-recycle-bin-menu-structure.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/media-recycle-bin-menu-structure.context.ts new file mode 100644 index 0000000000..0b015047e9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/menu/media-recycle-bin-menu-structure.context.ts @@ -0,0 +1,11 @@ +import { UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS } from '../tree/constants.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbMenuVariantTreeStructureWorkspaceContextBase } from '@umbraco-cms/backoffice/menu'; + +export class UmbMediaRecycleBinMenuStructureContext extends UmbMenuVariantTreeStructureWorkspaceContextBase { + constructor(host: UmbControllerHost) { + super(host, { treeRepositoryAlias: UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS }); + } +} + +export { UmbMediaRecycleBinMenuStructureContext as api }; From c666c00ac5f530cec6a707c3bad27a8ebd038811 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 20 Oct 2025 16:42:52 +0200 Subject: [PATCH 15/28] Document/Media Recycle Bin: Add missing root workspace views (#20494) (#20569) * add document recycle bin root workspace * add media recycle bin root workspace * export consts --- .../documents/recycle-bin/constants.ts | 2 +- .../documents/recycle-bin/manifests.ts | 2 + .../documents/recycle-bin/root/constants.ts | 2 + .../documents/recycle-bin/root/entity.ts | 3 + .../documents/recycle-bin/root/manifests.ts | 3 + .../recycle-bin/root/workspace/constants.ts | 1 + .../recycle-bin/root/workspace/manifests.ts | 35 ++++++ .../documents/recycle-bin/tree/constants.ts | 2 + .../documents/recycle-bin/tree/manifests.ts | 13 +-- .../collection/constants.ts | 3 + .../tree-item-children/collection/index.ts | 2 + .../collection/manifests.ts | 18 ++++ .../collection/repository/constants.ts | 2 + ...ree-item-children-collection.repository.ts | 33 ++++++ .../collection/repository/index.ts | 1 + .../collection/repository/manifests.ts | 10 ++ .../tree-item-children/collection/types.ts | 6 ++ ...tree-item-table-collection-view.element.ts | 101 ++++++++++++++++++ .../collection/views/manifests.ts | 23 ++++ ...shed-document-name-table-column.element.ts | 62 +++++++++++ .../tree/tree-item-children/constants.ts | 1 + .../tree/tree-item-children/manifests.ts | 3 + .../media/media/recycle-bin/constants.ts | 2 +- .../recycle-bin/entity-action/manifests.ts | 3 +- .../media/media/recycle-bin/manifests.ts | 2 + .../media/media/recycle-bin/root/constants.ts | 2 + .../media/media/recycle-bin/root/entity.ts | 3 + .../media/media/recycle-bin/root/manifests.ts | 3 + .../recycle-bin/root/workspace/constants.ts | 1 + .../recycle-bin/root/workspace/manifests.ts | 35 ++++++ .../media/media/recycle-bin/tree/constants.ts | 2 + .../media/media/recycle-bin/tree/manifests.ts | 14 +-- .../collection/constants.ts | 3 + .../tree-item-children/collection/index.ts | 2 + .../collection/manifests.ts | 18 ++++ .../collection/repository/constants.ts | 2 + .../collection/repository/index.ts | 1 + .../collection/repository/manifests.ts | 10 ++ ...ree-item-children-collection.repository.ts | 33 ++++++ .../tree-item-children/collection/types.ts | 6 ++ .../collection/views/manifests.ts | 23 ++++ ...tree-item-table-collection-view.element.ts | 101 ++++++++++++++++++ ...trashed-media-name-table-column.element.ts | 54 ++++++++++ .../tree/tree-item-children/constants.ts | 1 + .../tree/tree-item-children/manifests.ts | 3 + 45 files changed, 627 insertions(+), 25 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/entity.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/workspace/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/workspace/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/document-recycle-bin-tree-item-children-collection.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/document-recycle-bin-tree-item-table-collection-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/trashed-document-name-table-column.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/entity.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/workspace/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/workspace/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/media-recycle-bin-tree-item-children-collection.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/media-recycle-bin-tree-item-table-collection-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/trashed-media-name-table-column.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts index 82db473915..15ea9ece9b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts @@ -1,4 +1,4 @@ export * from './menu/constants.js'; export * from './repository/constants.js'; +export * from './root/constants.js'; export * from './tree/constants.js'; -export const UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'document-recycle-bin-root'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/manifests.ts index 55c305be6e..3bf5f764f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/manifests.ts @@ -1,6 +1,7 @@ import { manifests as entityActionManifests } from './entity-action/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as rootManifests } from './root/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; export const manifests: Array = [ @@ -13,5 +14,6 @@ export const manifests: Array = [ ...entityActionManifests, ...menuManifests, ...repositoryManifests, + ...rootManifests, ...treeManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/constants.ts new file mode 100644 index 0000000000..2fb39332b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/constants.ts @@ -0,0 +1,2 @@ +export { UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE } from './entity.js'; +export * from './workspace/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/entity.ts new file mode 100644 index 0000000000..56a25afd0f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/entity.ts @@ -0,0 +1,3 @@ +export const UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'document-recycle-bin-root'; + +export type UmbDocumentRecycleBinRootEntityType = typeof UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/manifests.ts new file mode 100644 index 0000000000..f3edde04ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as workspaceManifests } from './workspace/manifests.js'; + +export const manifests: Array = [...workspaceManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/workspace/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/workspace/constants.ts new file mode 100644 index 0000000000..7ed8ef9286 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/workspace/constants.ts @@ -0,0 +1 @@ +export const UMB_DOCUMENT_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS = 'Umb.Workspace.Document.RecycleBin.Root'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/workspace/manifests.ts new file mode 100644 index 0000000000..fb21ec4e50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/root/workspace/manifests.ts @@ -0,0 +1,35 @@ +import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS } from '../../tree/constants.js'; +import { UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../entity.js'; +import { UMB_DOCUMENT_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS } from './constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspace', + kind: 'default', + alias: UMB_DOCUMENT_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS, + name: 'Document Recycle Bin Root Workspace', + meta: { + entityType: UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE, + headline: '#general_recycleBin', + }, + }, + { + type: 'workspaceView', + kind: 'collection', + alias: 'Umb.WorkspaceView.DocumentRecycleBinRoot.Root', + name: 'Document Recycle Bin Root Collection Workspace View', + meta: { + label: 'Collection', + pathname: 'collection', + icon: 'icon-layers', + collectionAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS, + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/constants.ts index f6af6f8ac0..2eb9967b50 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/constants.ts @@ -6,3 +6,5 @@ export const UMB_DOCUMENT_RECYCLE_BIN_TREE_STORE_ALIAS = 'Umb.Store.Document.Rec export const UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS = 'Umb.Tree.Document.RecycleBin'; export { UMB_DOCUMENT_RECYCLE_BIN_TREE_STORE_CONTEXT } from './data/document-recycle-bin-tree.store.context-token.js'; + +export * from './tree-item-children/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/manifests.ts index c4b3774d97..17637d2cbd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/manifests.ts @@ -1,8 +1,8 @@ -import { UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../constants.js'; import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ALIAS, UMB_DOCUMENT_RECYCLE_BIN_TREE_REPOSITORY_ALIAS } from './constants.js'; import { manifests as dataManifests } from './data/manifests.js'; import { manifests as reloadTreeItemChildrenManifests } from './reload-tree-item-children/manifests.js'; import { manifests as rootTreeItemManifests } from './tree-item/manifests.js'; +import { manifests as treeItemChildrenManifests } from './tree-item-children/manifests.js'; export const manifests: Array = [ { @@ -14,17 +14,8 @@ export const manifests: Array = [ repositoryAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_REPOSITORY_ALIAS, }, }, - { - type: 'workspace', - kind: 'default', - alias: 'Umb.Workspace.Document.RecycleBin.Root', - name: 'Document Recycle Bin Root Workspace', - meta: { - entityType: UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE, - headline: '#general_recycleBin', - }, - }, ...dataManifests, ...reloadTreeItemChildrenManifests, ...rootTreeItemManifests, + ...treeItemChildrenManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/constants.ts new file mode 100644 index 0000000000..a5b3be6459 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/constants.ts @@ -0,0 +1,3 @@ +export const UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS = + 'Umb.Collection.DocumentRecycleBin.TreeItemChildren'; +export * from './repository/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/index.ts new file mode 100644 index 0000000000..6c11f6abbb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/index.ts @@ -0,0 +1,2 @@ +export * from './constants.js'; +export * from './repository/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/manifests.ts new file mode 100644 index 0000000000..356876e066 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/manifests.ts @@ -0,0 +1,18 @@ +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as viewManifests } from './views/manifests.js'; +import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS } from './constants.js'; +import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS } from './repository/index.js'; + +export const manifests: Array = [ + { + type: 'collection', + kind: 'default', + alias: UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS, + name: 'Document Recycle Bin Tree Item Children Collection', + meta: { + repositoryAlias: UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS, + }, + }, + ...repositoryManifests, + ...viewManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/constants.ts new file mode 100644 index 0000000000..9b43b21276 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/constants.ts @@ -0,0 +1,2 @@ +export const UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS = + 'Umb.Repository.DocumentRecycleBin.TreeItemChildrenCollection'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/document-recycle-bin-tree-item-children-collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/document-recycle-bin-tree-item-children-collection.repository.ts new file mode 100644 index 0000000000..a14b067aa8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/document-recycle-bin-tree-item-children-collection.repository.ts @@ -0,0 +1,33 @@ +import { UmbDocumentRecycleBinTreeRepository } from '../../../index.js'; +import type { UmbCollectionFilterModel, UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import { UMB_ENTITY_CONTEXT, type UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export class UmbDocumentRecycleBinTreeItemChildrenCollectionRepository + extends UmbRepositoryBase + implements UmbCollectionRepository +{ + #treeRepository = new UmbDocumentRecycleBinTreeRepository(this); + + async requestCollection(filter: UmbCollectionFilterModel) { + // TODO: get parent from args + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + if (!entityContext) throw new Error('Entity context not found'); + + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (!entityType) throw new Error('Entity type not found'); + if (unique === undefined) throw new Error('Unique not found'); + + const parent: UmbEntityModel = { entityType, unique }; + + if (parent.unique === null) { + return this.#treeRepository.requestTreeRootItems({ skip: filter.skip, take: filter.take }); + } else { + return this.#treeRepository.requestTreeItemsOf({ parent, skip: filter.skip, take: filter.take }); + } + } +} + +export { UmbDocumentRecycleBinTreeItemChildrenCollectionRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/index.ts new file mode 100644 index 0000000000..4f07201dcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/manifests.ts new file mode 100644 index 0000000000..437753a9af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/repository/manifests.ts @@ -0,0 +1,10 @@ +import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'repository', + alias: UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS, + name: 'Document Recycle Bin Tree Item Children Collection Repository', + api: () => import('./document-recycle-bin-tree-item-children-collection.repository.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/types.ts new file mode 100644 index 0000000000..ac2209ac74 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/types.ts @@ -0,0 +1,6 @@ +import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export interface UmbDocumentRecycleBinTreeItemChildrenCollectionFilterModel extends UmbCollectionFilterModel { + parent: UmbEntityModel; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/document-recycle-bin-tree-item-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/document-recycle-bin-tree-item-table-collection-view.element.ts new file mode 100644 index 0000000000..58feca6dd5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/document-recycle-bin-tree-item-table-collection-view.element.ts @@ -0,0 +1,101 @@ +import type { UmbDocumentRecycleBinTreeItemModel } from '../../../types.js'; +import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; +import type { UmbTableColumn, UmbTableConfig, UmbTableItem } from '@umbraco-cms/backoffice/components'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; + +import './trashed-document-name-table-column.element.js'; + +@customElement('umb-document-recycle-bin-tree-item-table-collection-view') +export class UmbDocumentRecycleBinTreeItemTableCollectionViewElement extends UmbLitElement { + @state() + private _tableConfig: UmbTableConfig = { + allowSelection: false, + }; + + @state() + private _tableColumns: Array = [ + { + name: this.localize.term('general_name'), + alias: 'name', + elementName: 'umb-trashed-document-name-table-column', + }, + { + name: '', + alias: 'entityActions', + align: 'right', + }, + ]; + + @state() + private _tableItems: Array = []; + + #collectionContext?: UmbDefaultCollectionContext; + #isTrashedContext = new UmbIsTrashedEntityContext(this); + + constructor() { + super(); + this.#isTrashedContext.setIsTrashed(true); + + this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; + this.#observeCollectionItems(); + }); + } + + #observeCollectionItems() { + if (!this.#collectionContext) return; + this.observe(this.#collectionContext.items, (items) => this.#createTableItems(items), 'umbCollectionItemsObserver'); + } + + #createTableItems(items: Array) { + this._tableItems = items.map((item) => { + return { + id: item.unique, + icon: item.documentType.icon, + data: [ + { + columnAlias: 'name', + value: item, + }, + { + columnAlias: 'entityActions', + value: html``, + }, + ], + }; + }); + } + + override render() { + return html` + + `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: flex; + flex-direction: column; + } + `, + ]; +} + +export { UmbDocumentRecycleBinTreeItemTableCollectionViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + ['umb-document-recycle-bin-tree-item-table-collection-view']: UmbDocumentRecycleBinTreeItemTableCollectionViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/manifests.ts new file mode 100644 index 0000000000..ef40a35fe2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS } from '../constants.js'; +import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; + +export const manifests: Array = [ + { + type: 'collectionView', + alias: 'Umb.CollectionView.DocumentRecycleBin.TreeItem.Table', + name: 'Document Recycle Bin Tree Item Table Collection View', + element: () => import('./document-recycle-bin-tree-item-table-collection-view.element.js'), + weight: 300, + meta: { + label: 'Table', + icon: 'icon-list', + pathName: 'table', + }, + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: UMB_DOCUMENT_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/trashed-document-name-table-column.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/trashed-document-name-table-column.element.ts new file mode 100644 index 0000000000..fffd779a9e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/collection/views/trashed-document-name-table-column.element.ts @@ -0,0 +1,62 @@ +import { UmbDocumentItemDataResolver } from '../../../../../item/index.js'; +import type { UmbDocumentRecycleBinTreeItemModel } from '../../../types.js'; +import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../../../paths.js'; +import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbTableColumn, UmbTableColumnLayoutElement, UmbTableItem } from '@umbraco-cms/backoffice/components'; + +@customElement('umb-trashed-document-name-table-column') +export class UmbTrashedDocumentNameTableColumnElement extends UmbLitElement implements UmbTableColumnLayoutElement { + #resolver = new UmbDocumentItemDataResolver(this); + + @state() + private _name = ''; + + @state() + private _editPath = ''; + + column!: UmbTableColumn; + item!: UmbTableItem; + + @property({ attribute: false }) + public set value(value: UmbDocumentRecycleBinTreeItemModel) { + this.#value = value; + + if (value) { + this.#resolver.setData(value); + + this._editPath = UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateAbsolute({ + unique: value.unique, + }); + } + } + public get value(): UmbDocumentRecycleBinTreeItemModel { + return this.#value; + } + #value!: UmbDocumentRecycleBinTreeItemModel; + + constructor() { + super(); + this.#resolver.observe(this.#resolver.name, (name) => (this._name = name || '')); + } + + override render() { + if (!this.value) return nothing; + if (!this._name) return nothing; + return html``; + } + + static override styles = [ + css` + uui-button { + text-align: left; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-trashed-document-name-table-column': UmbTrashedDocumentNameTableColumnElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/constants.ts new file mode 100644 index 0000000000..d7b529d49c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/constants.ts @@ -0,0 +1 @@ +export * from './collection/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/manifests.ts new file mode 100644 index 0000000000..bfab902d3c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/tree-item-children/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as collectionManifests } from './collection/manifests.js'; + +export const manifests: Array = [...collectionManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts index 49ba8c8836..6ba1d184de 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts @@ -1,4 +1,4 @@ export * from './menu/constants.js'; export * from './repository/constants.js'; export * from './tree/constants.js'; -export const UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'media-recycle-bin-root'; +export * from './root/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts index 144684b99c..a043847c99 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts @@ -2,9 +2,10 @@ import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL, UMB_MEDIA_ENTITY_TYPE, + UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS, } from '../../constants.js'; -import { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE, UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS } from '../constants.js'; import { UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS } from '../../reference/constants.js'; +import { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../root/entity.js'; import { manifests as bulkTrashManifests } from './bulk-trash/manifests.js'; import { UMB_ENTITY_HAS_CHILDREN_CONDITION_ALIAS } from '@umbraco-cms/backoffice/entity-action'; import { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts index 5f87f5fcf4..ff4bb517f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/manifests.ts @@ -1,6 +1,7 @@ import { manifests as entityActionManifests } from './entity-action/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as rootManifests } from './root/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; export const manifests: Array = [ @@ -13,5 +14,6 @@ export const manifests: Array = [ ...entityActionManifests, ...menuManifests, ...repositoryManifests, + ...rootManifests, ...treeManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/constants.ts new file mode 100644 index 0000000000..27491976dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/constants.ts @@ -0,0 +1,2 @@ +export { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE } from './entity.js'; +export * from './workspace/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/entity.ts new file mode 100644 index 0000000000..60448b8919 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/entity.ts @@ -0,0 +1,3 @@ +export const UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'media-recycle-bin-root'; + +export type UmbMediaRecycleBinRootEntityType = typeof UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/manifests.ts new file mode 100644 index 0000000000..f3edde04ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as workspaceManifests } from './workspace/manifests.js'; + +export const manifests: Array = [...workspaceManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/workspace/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/workspace/constants.ts new file mode 100644 index 0000000000..48a3bf749a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/workspace/constants.ts @@ -0,0 +1 @@ +export const UMB_MEDIA_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS = 'Umb.Workspace.Media.RecycleBin.Root'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/workspace/manifests.ts new file mode 100644 index 0000000000..22276ca6a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/root/workspace/manifests.ts @@ -0,0 +1,35 @@ +import { UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS } from '../../tree/constants.js'; +import { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../entity.js'; +import { UMB_MEDIA_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS } from './constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspace', + kind: 'default', + alias: UMB_MEDIA_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS, + name: 'Media Recycle Bin Root Workspace', + meta: { + entityType: UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE, + headline: '#general_recycleBin', + }, + }, + { + type: 'workspaceView', + kind: 'collection', + alias: 'Umb.WorkspaceView.Media.RecycleBin.Root', + name: 'Media Recycle Bin Root Collection Workspace View', + meta: { + label: 'Collection', + pathname: 'collection', + icon: 'icon-layers', + collectionAlias: UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS, + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_MEDIA_RECYCLE_BIN_ROOT_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/constants.ts index d5a44c7ff5..e84b5d520f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/constants.ts @@ -6,3 +6,5 @@ export const UMB_MEDIA_RECYCLE_BIN_TREE_STORE_ALIAS = 'Umb.Store.Media.RecycleBi export const UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS = 'Umb.Tree.Media.RecycleBin'; export { UMB_MEDIA_RECYCLE_BIN_TREE_STORE_CONTEXT } from './media-recycle-bin-tree.store.context-token.js'; + +export * from './tree-item-children/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/manifests.ts index 956585aea8..e51d86f3c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/manifests.ts @@ -1,11 +1,12 @@ import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; -import { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../constants.js'; +import { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../root/entity.js'; import { UMB_MEDIA_RECYCLE_BIN_TREE_ALIAS, UMB_MEDIA_RECYCLE_BIN_TREE_REPOSITORY_ALIAS, UMB_MEDIA_RECYCLE_BIN_TREE_STORE_ALIAS, } from './constants.js'; import { manifests as reloadTreeItemChildrenManifests } from './reload-tree-item-children/manifests.js'; +import { manifests as treeItemChildrenManifests } from './tree-item-children/manifests.js'; export const manifests: Array = [ { @@ -39,15 +40,6 @@ export const manifests: Array = [ supportedEntityTypes: [UMB_MEDIA_ENTITY_TYPE], }, }, - { - type: 'workspace', - kind: 'default', - alias: 'Umb.Workspace.Media.RecycleBin.Root', - name: 'Media Recycle Bin Root Workspace', - meta: { - entityType: UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE, - headline: '#general_recycleBin', - }, - }, ...reloadTreeItemChildrenManifests, + ...treeItemChildrenManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/constants.ts new file mode 100644 index 0000000000..334e56bcd8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/constants.ts @@ -0,0 +1,3 @@ +export const UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS = + 'Umb.Collection.Media.RecycleBin.TreeItemChildren'; +export * from './repository/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/index.ts new file mode 100644 index 0000000000..6c11f6abbb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/index.ts @@ -0,0 +1,2 @@ +export * from './constants.js'; +export * from './repository/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/manifests.ts new file mode 100644 index 0000000000..9239b10314 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/manifests.ts @@ -0,0 +1,18 @@ +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as viewManifests } from './views/manifests.js'; +import { UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS } from './constants.js'; +import { UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS } from './repository/index.js'; + +export const manifests: Array = [ + { + type: 'collection', + kind: 'default', + alias: UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS, + name: 'Media Recycle Bin Tree Item Children Collection', + meta: { + repositoryAlias: UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS, + }, + }, + ...repositoryManifests, + ...viewManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/constants.ts new file mode 100644 index 0000000000..2124b318d9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/constants.ts @@ -0,0 +1,2 @@ +export const UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS = + 'Umb.Repository.Media.RecycleBin.TreeItemChildrenCollection'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/index.ts new file mode 100644 index 0000000000..4f07201dcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/manifests.ts new file mode 100644 index 0000000000..8e354bc7a5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/manifests.ts @@ -0,0 +1,10 @@ +import { UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'repository', + alias: UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_REPOSITORY_ALIAS, + name: 'Media Recycle Bin Tree Item Children Collection Repository', + api: () => import('./media-recycle-bin-tree-item-children-collection.repository.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/media-recycle-bin-tree-item-children-collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/media-recycle-bin-tree-item-children-collection.repository.ts new file mode 100644 index 0000000000..bd0de3d126 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/repository/media-recycle-bin-tree-item-children-collection.repository.ts @@ -0,0 +1,33 @@ +import { UmbMediaRecycleBinTreeRepository } from '../../../index.js'; +import type { UmbCollectionFilterModel, UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import { UMB_ENTITY_CONTEXT, type UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export class UmbMediaRecycleBinTreeItemChildrenCollectionRepository + extends UmbRepositoryBase + implements UmbCollectionRepository +{ + #treeRepository = new UmbMediaRecycleBinTreeRepository(this); + + async requestCollection(filter: UmbCollectionFilterModel) { + // TODO: get parent from args + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + if (!entityContext) throw new Error('Entity context not found'); + + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (!entityType) throw new Error('Entity type not found'); + if (unique === undefined) throw new Error('Unique not found'); + + const parent: UmbEntityModel = { entityType, unique }; + + if (parent.unique === null) { + return this.#treeRepository.requestTreeRootItems({ skip: filter.skip, take: filter.take }); + } else { + return this.#treeRepository.requestTreeItemsOf({ parent, skip: filter.skip, take: filter.take }); + } + } +} + +export { UmbMediaRecycleBinTreeItemChildrenCollectionRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/types.ts new file mode 100644 index 0000000000..ec21427cb0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/types.ts @@ -0,0 +1,6 @@ +import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export interface UmbMediaRecycleBinTreeItemChildrenCollectionFilterModel extends UmbCollectionFilterModel { + parent: UmbEntityModel; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/manifests.ts new file mode 100644 index 0000000000..abf3fab48f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS } from '../constants.js'; +import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; + +export const manifests: Array = [ + { + type: 'collectionView', + alias: 'Umb.CollectionView.Media.RecycleBin.TreeItem.Table', + name: 'Media Recycle Bin Tree Item Table Collection View', + element: () => import('./media-recycle-bin-tree-item-table-collection-view.element.js'), + weight: 300, + meta: { + label: 'Table', + icon: 'icon-list', + pathName: 'table', + }, + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: UMB_MEDIA_RECYCLE_BIN_TREE_ITEM_CHILDREN_COLLECTION_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/media-recycle-bin-tree-item-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/media-recycle-bin-tree-item-table-collection-view.element.ts new file mode 100644 index 0000000000..a6fc251858 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/media-recycle-bin-tree-item-table-collection-view.element.ts @@ -0,0 +1,101 @@ +import type { UmbMediaRecycleBinTreeItemModel } from '../../../types.js'; +import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; +import type { UmbTableColumn, UmbTableConfig, UmbTableItem } from '@umbraco-cms/backoffice/components'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; + +import './trashed-media-name-table-column.element.js'; + +@customElement('umb-media-recycle-bin-tree-item-table-collection-view') +export class UmbMediaRecycleBinTreeItemTableCollectionViewElement extends UmbLitElement { + @state() + private _tableConfig: UmbTableConfig = { + allowSelection: false, + }; + + @state() + private _tableColumns: Array = [ + { + name: this.localize.term('general_name'), + alias: 'name', + elementName: 'umb-trashed-media-name-table-column', + }, + { + name: '', + alias: 'entityActions', + align: 'right', + }, + ]; + + @state() + private _tableItems: Array = []; + + #collectionContext?: UmbDefaultCollectionContext; + #isTrashedContext = new UmbIsTrashedEntityContext(this); + + constructor() { + super(); + this.#isTrashedContext.setIsTrashed(true); + + this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; + this.#observeCollectionItems(); + }); + } + + #observeCollectionItems() { + if (!this.#collectionContext) return; + this.observe(this.#collectionContext.items, (items) => this.#createTableItems(items), 'umbCollectionItemsObserver'); + } + + #createTableItems(items: Array) { + this._tableItems = items.map((item) => { + return { + id: item.unique, + icon: item.mediaType.icon, + data: [ + { + columnAlias: 'name', + value: item, + }, + { + columnAlias: 'entityActions', + value: html``, + }, + ], + }; + }); + } + + override render() { + return html` + + `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: flex; + flex-direction: column; + } + `, + ]; +} + +export { UmbMediaRecycleBinTreeItemTableCollectionViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + ['umb-media-recycle-bin-tree-item-table-collection-view']: UmbMediaRecycleBinTreeItemTableCollectionViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/trashed-media-name-table-column.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/trashed-media-name-table-column.element.ts new file mode 100644 index 0000000000..3a797d224f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/collection/views/trashed-media-name-table-column.element.ts @@ -0,0 +1,54 @@ +import type { UmbMediaRecycleBinTreeItemModel } from '../../../types.js'; +import { UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../../../../../paths.js'; +import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbTableColumn, UmbTableColumnLayoutElement, UmbTableItem } from '@umbraco-cms/backoffice/components'; + +@customElement('umb-trashed-media-name-table-column') +export class UmbTrashedMediaNameTableColumnElement extends UmbLitElement implements UmbTableColumnLayoutElement { + @state() + private _name = ''; + + @state() + private _editPath = ''; + + column!: UmbTableColumn; + item!: UmbTableItem; + + @property({ attribute: false }) + public set value(value: UmbMediaRecycleBinTreeItemModel) { + this.#value = value; + + if (value) { + this._name = value.variants[0]?.name || ''; + + this._editPath = UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN.generateAbsolute({ + unique: value.unique, + }); + } + } + public get value(): UmbMediaRecycleBinTreeItemModel { + return this.#value; + } + #value!: UmbMediaRecycleBinTreeItemModel; + + override render() { + if (!this.value) return nothing; + if (!this._name) return nothing; + return html``; + } + + static override styles = [ + css` + uui-button { + text-align: left; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-trashed-media-name-table-column': UmbTrashedMediaNameTableColumnElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/constants.ts new file mode 100644 index 0000000000..d7b529d49c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/constants.ts @@ -0,0 +1 @@ +export * from './collection/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/manifests.ts new file mode 100644 index 0000000000..bfab902d3c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/tree-item-children/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as collectionManifests } from './collection/manifests.js'; + +export const manifests: Array = [...collectionManifests]; From 1efdde2473a0ee3505d5f884a266368d86027bb7 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 16/28] 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 2e8f7a23db..9c08dfb59f 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" @@ -16339,9 +16339,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 ca5cc8853a..b22f6f4e74 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -263,7 +263,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 e6f48799a1ee836d033ed82397dd9ea1b1b87372 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:29:46 +0200 Subject: [PATCH 17/28] Property Editors: DateTimeWithTimeZone - Changing timezone mode to Local shows invalid time zone error (#20526) * Store local time zone as UTC and do not throw validation error when stored time zone is different * Additional fixes when switching between date time editors with and without time zone * Additional fixes * Ensure that an update is triggered when the expected value does not match the stored value This will happen when switching between editors (with and without time zone) or switching between a specific time zone to the editor's local time zone. * Fix inconsistencies with null and undefined * Fix inconsistencies between date/time provided to the client and returned in the value converter (when switching between editors) * Fix unit tests and small bug * Adjust integration test * Small improvement * Update test data * Adjust logic so that time zone offsets are updated every time the date value changes * Do not pre-select time zone when switching between unspecified and time zone editors --- .../ValueConverters/DateOnlyValueConverter.cs | 2 +- .../DateTimeUnspecifiedValueConverter.cs | 3 +- .../ValueConverters/TimeOnlyValueConverter.cs | 2 +- ...roperty-editor-ui-date-time-picker-base.ts | 100 ++++++++++++------ .../DateTimePropertyEditorTests.cs | 8 +- .../DateTimeUnspecifiedValueConverterTests.cs | 2 +- .../TimeOnlyValueConverterTests.cs | 2 +- 7 files changed, 78 insertions(+), 41 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateOnlyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateOnlyValueConverter.cs index cdb2339478..a01e39e6fe 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateOnlyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateOnlyValueConverter.cs @@ -30,5 +30,5 @@ public class DateOnlyValueConverter : DateTimeValueConverterBase /// protected override object ConvertToObject(DateTimeDto dateTimeDto) - => DateOnly.FromDateTime(dateTimeDto.Date.UtcDateTime); + => DateOnly.FromDateTime(dateTimeDto.Date.DateTime); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverter.cs index da793aeb2f..9bd138e591 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverter.cs @@ -30,6 +30,5 @@ public class DateTimeUnspecifiedValueConverter : DateTimeValueConverterBase /// protected override object ConvertToObject(DateTimeDto dateTimeDto) - => DateTime.SpecifyKind(dateTimeDto.Date.UtcDateTime, DateTimeKind.Unspecified); - + => DateTime.SpecifyKind(dateTimeDto.Date.DateTime, DateTimeKind.Unspecified); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/TimeOnlyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/TimeOnlyValueConverter.cs index f6862e5b6f..777a20f048 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/TimeOnlyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/TimeOnlyValueConverter.cs @@ -30,5 +30,5 @@ public class TimeOnlyValueConverter : DateTimeValueConverterBase /// protected override object ConvertToObject(DateTimeDto dateTimeDto) - => TimeOnly.FromDateTime(dateTimeDto.Date.UtcDateTime); + => TimeOnly.FromDateTime(dateTimeDto.Date.DateTime); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts index 8c0b1ce9aa..07e196850a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts @@ -19,8 +19,8 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui'; interface UmbDateTime { - date: string | undefined; - timeZone: string | undefined; + date: string | null; + timeZone: string | null; } interface UmbTimeZonePickerOption extends UmbTimeZone { @@ -34,6 +34,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase { private _timeZoneOptions: Array = []; private _clientTimeZone: UmbTimeZone | undefined; + private _timeZoneMode: UmbTimeZonePickerValue['mode'] | undefined; @property({ type: Boolean, reflect: true }) readonly = false; @@ -104,6 +105,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase () => { return ( this._displayTimeZone && + this._timeZoneMode !== 'local' && !!this.value?.timeZone && !this._timeZoneOptions.some((opt) => opt.value === this.value?.timeZone && !opt.invalid) ); @@ -120,8 +122,13 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase if (this._displayTimeZone) { timeZonePickerConfig = config.getValueByAlias('timeZones'); } + this.#setTimeInputStep(timeFormat); this.#prefillValue(timeZonePickerConfig); + + // To ensure the expected value matches the prefilled value, we trigger an update. + // If the values match, no change event will be fired. + this.#updateValue(this._selectedDate?.toISO({ includeOffset: false }) ?? null); } #prefillValue(timeZonePickerConfig: UmbTimeZonePickerValue | undefined) { @@ -158,8 +165,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase #prefillTimeZones(config: UmbTimeZonePickerValue | undefined, selectedDate: DateTime | undefined) { // Retrieve the time zones from the config this._clientTimeZone = getClientTimeZone(); + this._timeZoneMode = config?.mode; - // Retrieve the time zones from the config const dateToCalculateOffset = selectedDate ?? DateTime.now(); switch (config?.mode) { case 'all': @@ -219,7 +226,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase return; } } else if (this.value?.date) { - return; // If there is a date but no time zone, we don't preselect anything + // If there is no time zone in the value, but there is a date, we leave the time zone unselected + return; } // Check if we can pre-select the client time zone @@ -269,16 +277,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase return; } - if (!newPickerValue) { - this._datePickerValue = ''; - this.value = undefined; - this._selectedDate = undefined; - this.dispatchEvent(new UmbChangeEvent()); - return; - } - this._datePickerValue = newPickerValue; - this.#updateValue(value, true); + this.#updateValue(value); } #onTimeZoneChange(event: UUIComboboxEvent) { @@ -291,7 +291,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase if (!this._selectedTimeZone) { if (this.value?.date) { - this.value = { date: this.value.date, timeZone: undefined }; + this.value = { date: this.value.date, timeZone: null }; } else { this.value = undefined; } @@ -303,46 +303,84 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase return; } - this.#updateValue(this._selectedDate.toISO({ includeOffset: false }) || ''); + this.#updateValue(this._selectedDate.toISO({ includeOffset: false })); } - #updateValue(date: string, updateOffsets = false) { + #updateValue(date: string | null) { // Try to parse the date with the selected time zone - const newDate = DateTime.fromISO(date, { zone: this._selectedTimeZone ?? 'UTC' }); + const newDate = date ? DateTime.fromISO(date, { zone: this._selectedTimeZone || 'UTC' }) : null; // If the date is invalid, we reset the value - if (!newDate.isValid) { + if (!newDate || !newDate.isValid) { + if (!this.value) { + return; // No change + } this.value = undefined; this._selectedDate = undefined; this.dispatchEvent(new UmbChangeEvent()); + this.#updateOffsets(DateTime.now()); return; } + const previousDate = this._selectedDate; this._selectedDate = newDate; - this.value = { - date: this.#getCurrentDateValue(), - timeZone: this._selectedTimeZone, + + let timeZoneToStore = null; + if (!this._displayTimeZone || !this._timeZoneMode) { + timeZoneToStore = null; + } else if (this._timeZoneMode === 'local') { + timeZoneToStore = 'UTC'; + } else { + timeZoneToStore = this._selectedTimeZone ?? null; + } + + const dateToStore = + timeZoneToStore && this._selectedTimeZone !== timeZoneToStore ? newDate.setZone(timeZoneToStore) : newDate; + + const newValue = { + date: this.#formatDateValue(dateToStore), + timeZone: timeZoneToStore, }; - if (updateOffsets) { - this._timeZoneOptions.forEach((opt) => { - opt.offset = getTimeZoneOffset(opt.value, newDate); - }); - // Update the time zone options (mostly for the offset) - this._filteredTimeZoneOptions = this._timeZoneOptions; + // Only update the stored data if it has actually changed to avoid firing unnecessary change events + const previousValue = this.value; + if (previousValue?.date === newValue.date && previousValue?.timeZone === newValue.timeZone) { + return; } + + this.value = newValue; this.dispatchEvent(new UmbChangeEvent()); + + // Only update offsets if the date timestamp has changed + if (previousDate?.toUnixInteger() !== newDate.toUnixInteger()) { + this.#updateOffsets(newDate); + } } - #getCurrentDateValue(): string | undefined { + #updateOffsets(date: DateTime) { + if (!this._displayTimeZone) return; + this._timeZoneOptions.forEach((opt) => { + opt.offset = getTimeZoneOffset(opt.value, date); + }); + // Update the time zone options (mostly for the offset) + this._filteredTimeZoneOptions = this._timeZoneOptions; + } + + #formatDateValue(date: DateTime): string | null { + let formattedDate: string | undefined; switch (this._dateInputType) { case 'date': - return this._selectedDate?.toISODate() ?? undefined; + formattedDate = date.toFormat('yyyy-MM-dd'); + break; case 'time': - return this._selectedDate?.toISOTime({ includeOffset: false }) ?? undefined; + formattedDate = date.toFormat('HH:mm:ss'); + break; default: - return this._selectedDate?.toISO({ includeOffset: !!this._selectedTimeZone }) ?? undefined; + formattedDate = date.toFormat(`yyyy-MM-dd'T'HH:mm:ss${this._timeZoneMode ? 'ZZ' : ''}`); + break; } + + return formattedDate ?? null; } #onTimeZoneSearch(event: UUIComboboxEvent) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs index 2e5135f1ab..48f8a035be 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs @@ -38,10 +38,10 @@ public class DateTimePropertyEditorTests : UmbracoIntegrationTest private static readonly object[] _sourceList1 = [ - new object[] { Constants.PropertyEditors.Aliases.DateOnly, false, new DateOnly(2025, 1, 22) }, + new object[] { Constants.PropertyEditors.Aliases.DateOnly, false, new DateOnly(2025, 6, 22) }, new object[] { Constants.PropertyEditors.Aliases.TimeOnly, false, new TimeOnly(18, 33, 1) }, - new object[] { Constants.PropertyEditors.Aliases.DateTimeUnspecified, false, new DateTime(2025, 1, 22, 18, 33, 1) }, - new object[] { Constants.PropertyEditors.Aliases.DateTimeWithTimeZone, true, new DateTimeOffset(2025, 1, 22, 18, 33, 1, TimeSpan.Zero) }, + new object[] { Constants.PropertyEditors.Aliases.DateTimeUnspecified, false, new DateTime(2025, 6, 22, 18, 33, 1) }, + new object[] { Constants.PropertyEditors.Aliases.DateTimeWithTimeZone, true, new DateTimeOffset(2025, 6, 22, 18, 33, 1, TimeSpan.FromHours(2)) }, ]; [TestCaseSource(nameof(_sourceList1))] @@ -105,7 +105,7 @@ public class DateTimePropertyEditorTests : UmbracoIntegrationTest .WithValue( new JsonObject { - ["date"] = "2025-01-22T18:33:01.0000000+00:00", + ["date"] = "2025-06-22T18:33:01.0000000+02:00", ["timeZone"] = "Europe/Copenhagen", }) .Done() diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs index dd7395f5d0..1f38c034f2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs @@ -86,7 +86,7 @@ public class DateTimeUnspecifiedValueConverterTests private static object[] _dateTimeUnspecifiedConvertToObjectCases = [ new object[] { null, null }, - new object[] { _convertToObjectInputDate, DateTime.Parse("2025-08-20T17:30:00") }, + new object[] { _convertToObjectInputDate, DateTime.Parse("2025-08-20T16:30:00") }, ]; [TestCaseSource(nameof(_dateTimeUnspecifiedConvertToObjectCases))] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs index a0bcd82ec9..ee3da51671 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs @@ -86,7 +86,7 @@ public class TimeOnlyValueConverterTests private static object[] _timeOnlyConvertToObjectCases = [ new object[] { null, null }, - new object[] { _convertToObjectInputDate, TimeOnly.Parse("17:30") }, + new object[] { _convertToObjectInputDate, TimeOnly.Parse("16:30") }, ]; [TestCaseSource(nameof(_timeOnlyConvertToObjectCases))] From 68d1b9481a05bd5ee0a959f033f28eb68df2c77d Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 21 Oct 2025 09:57:29 +0200 Subject: [PATCH 18/28] 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. (cherry picked from commit 81a8a0c191a56921e2bda536e50d01e95910324d) --- .../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 ca625dacdf..e97c60fd62 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 class UmbracoBuilderExtensions 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 ee1b7aefda..ccd5897494 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 @@ internal static class HybridCacheExtensions /// 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); + // 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)); - // 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) + 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); - return (exists, result); + // 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(); + + // 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 0581bd2654..38a6618c70 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 @@ using Umbraco.Cms.Infrastructure.HybridCache.Services; 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 @@ internal sealed class SeedingNotificationHandler : INotificationAsyncHandler(cacheKey); + var existsInCache = await _hybridCache.ExistsAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (existsInCache is false) { uncachedKeys.Add(key); @@ -278,7 +278,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService 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 65b8f91945..46d782bdbe 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs @@ -133,7 +133,7 @@ internal sealed class MediaCacheService : IMediaCacheService 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 @@ internal sealed class MediaCacheService : IMediaCacheService 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 1aec16e4a0..45d192587f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Notifications; 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; @@ -26,10 +27,10 @@ internal sealed class DocumentHybridCacheTests : UmbracoIntegrationTestWithConte 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"; @@ -467,6 +468,61 @@ internal sealed class DocumentHybridCacheTests : UmbracoIntegrationTestWithConte 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 152fe28b4e..8da30cd118 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 class HybridCacheExtensionsTests _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 class HybridCacheExtensionsTests _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 class HybridCacheExtensionsTests _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 class HybridCacheExtensionsTests _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 class HybridCacheExtensionsTests _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 class HybridCacheExtensionsTests 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 class HybridCacheExtensionsTests }); // 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 d05add4e1f3b2c3dad892a7d8614505dba4f7e8c Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Tue, 21 Oct 2025 08:28:01 +0100 Subject: [PATCH 19/28] 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 | 62 ++++++++++++------- .../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, 79 insertions(+), 69 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 0598f61223..3a44356d53 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 47523bb921..86ec324126 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 60c0894e84..b6a98f1ddf 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 ad71fe5568..89ac81a68f 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} -
    - - `; + 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 8b3e29cf6e..84844d673f 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 bb000b6d7c..cebfc2463c 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 cc7b02d45c..dd8eba4ac8 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 303d2f543d..92de056d94 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 af3b17243b665f75d601f3d4a755c32462359100 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 21 Oct 2025 11:38:01 +0200 Subject: [PATCH 20/28] 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. # Conflicts: # src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs --- .../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 52657a21e0..a5fc5cb67c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -329,7 +329,7 @@ public class MediaRepository : ContentRepositoryBase Date: Tue, 21 Oct 2025 12:05:10 +0200 Subject: [PATCH 21/28] 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 8903cac4f0..f32cd4121a 100644 --- a/src/Umbraco.Core/Services/ContentPublishingService.cs +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Models.ContentEditing; 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 @@ internal sealed class ContentPublishingService : IContentPublishingService IOptionsMonitor optionsMonitor, IRelationService relationService, ILogger logger, - ILongRunningOperationService longRunningOperationService) + ILongRunningOperationService longRunningOperationService, + IUmbracoContextFactory umbracoContextFactory) { _coreScopeProvider = coreScopeProvider; _contentService = contentService; @@ -53,6 +56,7 @@ internal sealed class ContentPublishingService : IContentPublishingService { _contentSettings = contentSettings; }); + _umbracoContextFactory = umbracoContextFactory; } /// @@ -280,7 +284,7 @@ internal sealed class ContentPublishingService : IContentPublishingService 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), @@ -314,6 +318,10 @@ internal sealed class ContentPublishingService : IContentPublishingService Guid userKey, bool returnContent) { + // Ensure we have an UmbracoContext in case running on a background thread so operations that run in the published notification handlers + // have access to this (e.g. webhooks). + using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); IContent? content = _contentService.GetById(key); if (content is null) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 9cb5d57474..0218a10b63 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Text.Json; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; @@ -53,10 +54,10 @@ public class ImageCropperValueConverter : PropertyValueConverterBase, IDeliveryA { value = _jsonSerializer.Deserialize(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 1099332edbdc1a4de46a6365b329ee25b552bea8 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 21 Oct 2025 12:42:22 +0200 Subject: [PATCH 22/28] Fixes broken integration test for v17 release (#20582) Fixes broken integration test Co-authored-by: Kenn Jacobsen --- .../UmbracoIntegrationTestWithContentEditing.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs index 86e1508801..fdf02388a9 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs @@ -34,9 +34,7 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat protected ContentCreateModel Textpage { get; private set; } - protected ContentScheduleCollection ContentSchedule { get; private set; } - - protected CultureAndScheduleModel CultureAndSchedule { get; private set; } + protected ICollection CultureAndSchedule { get; private set; } protected int TextpageId { get; private set; } @@ -91,11 +89,7 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat } // Sets the culture and schedule for the content, in this case, we are publishing immediately for all cultures - ContentSchedule = new ContentScheduleCollection(); - CultureAndSchedule = new CultureAndScheduleModel - { - CulturesToPublishImmediately = new HashSet { "*" }, Schedules = ContentSchedule, - }; + CultureAndSchedule = [new CulturePublishScheduleModel { Culture = "*", Schedule = null }]; // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 PublishedTextPage = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Published Page"); From 20eedda4c73f87429663b56c7414dbfdfcecef0f Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:12:25 +0200 Subject: [PATCH 23/28] Template: Update dotnet project template to remove previous LTS checks (#20573) * Remove now unnecessary LTS checks from project dotnet template * Update the starter kit versions --- src/Umbraco.Web.UI/Program.cs | 3 --- .../UmbracoProject/.template.config/starterkits.template.json | 4 ++-- templates/UmbracoProject/Dockerfile | 3 --- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI/Program.cs b/src/Umbraco.Web.UI/Program.cs index ad68d28351..a9834ed784 100644 --- a/src/Umbraco.Web.UI/Program.cs +++ b/src/Umbraco.Web.UI/Program.cs @@ -25,9 +25,6 @@ app.UseUmbraco() }) .WithEndpoints(u => { - /*#if (UmbracoRelease = 'LTS') - u.UseInstallerEndpoints(); - #endif */ u.UseBackOfficeEndpoints(); u.UseWebsiteEndpoints(); }); diff --git a/templates/UmbracoProject/.template.config/starterkits.template.json b/templates/UmbracoProject/.template.config/starterkits.template.json index 745eef20ae..4dbb28c005 100644 --- a/templates/UmbracoProject/.template.config/starterkits.template.json +++ b/templates/UmbracoProject/.template.config/starterkits.template.json @@ -34,11 +34,11 @@ "cases": [ { "condition": "(StarterKit == 'Umbraco.TheStarterKit' && (UmbracoRelease == 'Latest' || UmbracoRelease == 'Custom'))", - "value": "16.0.0" + "value": "17.0.0-rc" }, { "condition": "(StarterKit == 'Umbraco.TheStarterKit' && UmbracoRelease == 'LTS')", - "value": "13.0.0" + "value": "17.0.0" } ] } diff --git a/templates/UmbracoProject/Dockerfile b/templates/UmbracoProject/Dockerfile index 40347ec2ec..9f2fcbe193 100644 --- a/templates/UmbracoProject/Dockerfile +++ b/templates/UmbracoProject/Dockerfile @@ -26,8 +26,5 @@ USER root RUN mkdir umbraco RUN mkdir umbraco/Logs RUN chown $APP_UID umbraco --recursive -#if (UmbracoRelease = 'LTS') -RUN chown $APP_UID wwwroot/umbraco --recursive -#endif USER $APP_UID ENTRYPOINT ["dotnet", "UmbracoProject.dll"] From 5f1c65e7eafa9aec1435aadba5b560d5fccd2a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 21 Oct 2025 14:10:41 +0200 Subject: [PATCH 24/28] User group: permissions grouping (#20584) * clean up * localizations * group user permission by entity type * adjustments * fix lint errors * Support granular permissions without entity type Updated granular permission handling to allow permissions that are not tied to a specific entity type. Adjusted rendering logic and manifest interface to support undefined or empty forEntityTypes, and added UI for displaying ungrouped granular permissions. * revert for now --------- Co-authored-by: Mads Rasmussen Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .../user-permission/localization/en.ts | 2 +- .../src/assets/lang/da.ts | 9 +- .../src/assets/lang/en.ts | 9 +- .../document-property-value/manifests.ts | 3 +- ...cument-granular-user-permission.element.ts | 10 +- .../user-permissions/document/manifests.ts | 8 +- ...tity-type-granular-permissions.element.ts} | 23 ++++- ...p-entity-type-permission-groups.element.ts | 92 +++++++++++++++++++ ...r-group-entity-type-permissions.element.ts | 60 ++++++++++++ ...oup-entity-user-permission-list.element.ts | 84 ----------------- .../user-group-workspace-editor.element.ts | 57 +++++------- .../input-entity-user-permission.element.ts | 50 +++++----- ...ty-user-permission-settings-modal.token.ts | 1 + .../user-granular-permission.extension.ts | 1 + 14 files changed, 243 insertions(+), 166 deletions(-) rename src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/{user-group-granular-permission-list.element.ts => user-group-entity-type-granular-permissions.element.ts} (81%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permission-groups.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permissions.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-user-permission-list.element.ts diff --git a/src/Umbraco.Web.UI.Client/examples/user-permission/localization/en.ts b/src/Umbraco.Web.UI.Client/examples/user-permission/localization/en.ts index bf0bbd9046..ff52ddc79a 100644 --- a/src/Umbraco.Web.UI.Client/examples/user-permission/localization/en.ts +++ b/src/Umbraco.Web.UI.Client/examples/user-permission/localization/en.ts @@ -1,6 +1,6 @@ export default { user: { // eslint-disable-next-line @typescript-eslint/naming-convention - permissionsEntityGroup_dictionary: 'Dictionary', + permissionsEntityGroup_dictionary: 'Dictionary permissions', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts index fad3f6064c..2b08aaf2f5 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts @@ -80,6 +80,7 @@ export default { content: 'Indhold', administration: 'Administration', structure: 'Struktur', + general: 'Generelt', other: 'Andet', }, actionDescriptions: { @@ -2082,10 +2083,10 @@ export default { permissionsGranularHelp: 'Sæt rettigheder for specifikke noder', granularRightsLabel: 'Dokumenter', granularRightsDescription: 'Tillad adgang til specifikke dokumenter', - permissionsEntityGroup_document: 'Indhold', - permissionsEntityGroup_media: 'Medie', - permissionsEntityGroup_member: 'Medlemmer', - 'permissionsEntityGroup_document-property-value': 'Dokumentegenskabsværdi', + permissionsEntityGroup_document: 'Indholdsrettigheder', + permissionsEntityGroup_media: 'Medierettigheder', + permissionsEntityGroup_member: 'Medlemsrettigheder', + 'permissionsEntityGroup_document-property-value': 'Feltrettigheder', permissionNoVerbs: 'Ingen tilladte rettigheder', profile: 'Profil', searchAllChildren: "Søg alle 'børn'", 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 20bfbf1a25..d91f0234bb 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -80,6 +80,7 @@ export default { content: 'Content', administration: 'Administration', structure: 'Structure', + general: 'General', other: 'Other', }, actionDescriptions: { @@ -2104,10 +2105,10 @@ export default { permissionsGranularHelp: 'Set permissions for specific nodes', granularRightsLabel: 'Documents', granularRightsDescription: 'Assign permissions to specific documents', - permissionsEntityGroup_document: 'Document', - permissionsEntityGroup_media: 'Media', - permissionsEntityGroup_member: 'Member', - 'permissionsEntityGroup_document-property-value': 'Document Property Value', + permissionsEntityGroup_document: 'Document permissions', + permissionsEntityGroup_media: 'Media permissions', + permissionsEntityGroup_member: 'Member permissions', + 'permissionsEntityGroup_document-property-value': 'Document Property Value permissions', permissionNoVerbs: 'No allowed permissions', profile: 'Profile', searchAllChildren: 'Search all children', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/manifests.ts index 3141019a4a..4f608eaa67 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/manifests.ts @@ -39,13 +39,14 @@ export const manifests: Array = alias: 'Umb.UserGranularPermission.Document.PropertyValue', name: 'Document Property Values Granular User Permission', weight: 950, + forEntityTypes: [UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE], element: () => import( './input-document-property-value-user-permission/input-document-property-value-user-permission.element.js' ), meta: { schemaType: 'DocumentPropertyValuePermissionPresentationModel', - label: 'Document Property Values', + label: '#user_permissionsGranular', description: 'Assign permissions to Document property values', }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/input-document-granular-user-permission/input-document-granular-user-permission.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/input-document-granular-user-permission/input-document-granular-user-permission.element.ts index 4aacd927b4..a71c2f1db8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/input-document-granular-user-permission/input-document-granular-user-permission.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/input-document-granular-user-permission/input-document-granular-user-permission.element.ts @@ -28,7 +28,7 @@ export class UmbInputDocumentGranularUserPermissionElement extends UUIFormContro } @property({ type: Array, attribute: false }) - fallbackPermissions: Array = []; + public fallbackPermissions: Array = []; @state() private _items?: Array; @@ -50,7 +50,13 @@ export class UmbInputDocumentGranularUserPermissionElement extends UUIFormContro async #observePickedDocuments(uniques: Array) { const { asObservable } = await this.#documentItemRepository.requestItems(uniques); - this.observe(asObservable?.(), (items) => (this._items = items), 'observeItems'); + this.observe( + asObservable?.(), + (items) => { + this._items = items; + }, + 'observeItems', + ); } async #editGranularPermission(item: UmbDocumentItemModel) { 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 03bebae424..2801a95214 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 @@ -6,7 +6,6 @@ import { UMB_USER_PERMISSION_DOCUMENT_CREATE, UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS, UMB_USER_PERMISSION_DOCUMENT_PUBLISH, - UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, UMB_USER_PERMISSION_DOCUMENT_UPDATE, UMB_USER_PERMISSION_DOCUMENT_DUPLICATE, @@ -91,7 +90,7 @@ const permissions: Array = [ description: '#actionDescriptions_publish', }, }, - { + /*{ type: 'entityUserPermission', alias: UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, name: 'Document Permissions User Permission', @@ -101,7 +100,7 @@ const permissions: Array = [ label: '#actions_setPermissions', description: '#actionDescriptions_rights', }, - }, + },*/ { type: 'entityUserPermission', alias: UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, @@ -204,11 +203,12 @@ export const granularPermissions: Array = [ alias: 'Umb.UserGranularPermission.Document', name: 'Document Granular User Permission', weight: 1000, + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], element: () => import('./input-document-granular-user-permission/input-document-granular-user-permission.element.js'), meta: { schemaType: 'DocumentPermissionPresentationModel', - label: '#user_granularRightsLabel', + label: '#user_permissionsGranular', description: '{#user_granularRightsDescription}', }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-granular-permissions.element.ts similarity index 81% rename from src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-granular-permissions.element.ts index 3721b89985..9fb9d37715 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-granular-permissions.element.ts @@ -2,12 +2,15 @@ import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.contex import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api'; import type { ManifestGranularUserPermission } from '@umbraco-cms/backoffice/user-permission'; -import { html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, state, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { filterFrozenArray } from '@umbraco-cms/backoffice/observable-api'; -@customElement('umb-user-group-granular-permission-list') +@customElement('umb-user-group-entity-type-granular-permissions') export class UmbUserGroupGranularPermissionListElement extends UmbLitElement { + @property() + public entityType?: string; + @state() private _userGroupPermissions?: Array; @@ -57,9 +60,21 @@ export class UmbUserGroupGranularPermissionListElement extends UmbLitElement { override render() { if (!this._userGroupPermissions) return; + + if (!this.entityType) { + return html` + + manifest.forEntityTypes === undefined || manifest.forEntityTypes?.length === 0} + .renderMethod=${this.#renderProperty}> + `; + } + return html` + manifest.forEntityTypes?.includes(this.entityType!) || manifest.forEntityTypes?.length === 0} .renderMethod=${this.#renderProperty}>`; } @@ -95,6 +110,6 @@ export default UmbUserGroupGranularPermissionListElement; declare global { interface HTMLElementTagNameMap { - 'umb-user-group-granular-permission-list': UmbUserGroupGranularPermissionListElement; + 'umb-user-group-entity-type-granular-permissions': UmbUserGroupGranularPermissionListElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permission-groups.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permission-groups.element.ts new file mode 100644 index 0000000000..2d2d09dff9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permission-groups.element.ts @@ -0,0 +1,92 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { html, customElement, state, repeat, css, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +import './user-group-entity-type-permissions.element.js'; +import './user-group-entity-type-granular-permissions.element.js'; + +@customElement('umb-user-group-entity-type-permission-groups') +export class UmbUserGroupEntityTypePermissionGroupsElement extends UmbLitElement { + @state() + private _groups: Array<{ entityType: string; headline: string }> = []; + + @state() + private _hasGranularPermissionsWithNoEntityType = false; + + constructor() { + super(); + + this.observe( + umbExtensionsRegistry.byType('entityUserPermission'), + (manifests) => { + const entityTypes = [...new Set(manifests.flatMap((manifest) => manifest.forEntityTypes))]; + this._groups = entityTypes + .map((entityType) => { + return { + entityType, + headline: this.localize.term(`user_permissionsEntityGroup_${entityType}`), + }; + }) + .sort((a, b) => a.headline.localeCompare(b.headline)); + }, + 'umbUserPermissionsObserver', + ); + + this.#observeGranularPermissionsWithNoEntityType(); + } + + #observeGranularPermissionsWithNoEntityType() { + this.observe( + umbExtensionsRegistry.byTypeAndFilter( + 'userGranularPermission', + (manifest) => manifest.forEntityTypes === undefined || manifest.forEntityTypes.length === 0, + ), + (manifests) => { + this._hasGranularPermissionsWithNoEntityType = manifests.length > 0; + }, + ); + } + + override render() { + return html`${repeat( + this._groups, + (group) => group.entityType, + (group) => + html` +
    ${group.headline}
    + + + + +
    `, + )} + ${this.#renderUngroupedGranularPermissions()}`; + } + + #renderUngroupedGranularPermissions() { + if (!this._hasGranularPermissionsWithNoEntityType) return nothing; + return html` + `; + } + + static override styles = [ + UmbTextStyles, + css` + uui-box { + margin-top: var(--uui-size-space-6); + } + `, + ]; +} + +export default UmbUserGroupEntityTypePermissionGroupsElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-group-entity-type-permission-groups': UmbUserGroupEntityTypePermissionGroupsElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permissions.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permissions.element.ts new file mode 100644 index 0000000000..0edd3e9c33 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permissions.element.ts @@ -0,0 +1,60 @@ +import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context-token.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { html, customElement, state, nothing, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event'; + +@customElement('umb-user-group-entity-type-permissions') +export class UmbUserGroupEntityTypePermissionsElement extends UmbLitElement { + @property() + public entityType?: string; + + @state() + private _fallBackPermissions?: Array; + + #userGroupWorkspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => { + this.#userGroupWorkspaceContext = instance; + this.observe( + this.#userGroupWorkspaceContext?.fallbackPermissions, + (fallbackPermissions) => { + this._fallBackPermissions = fallbackPermissions; + }, + 'umbUserGroupEntityUserPermissionsObserver', + ); + }); + } + + #onPermissionChange(event: UmbSelectionChangeEvent) { + event.stopPropagation(); + const target = event.target as any; + const verbs = target.allowedVerbs; + if (verbs === undefined || verbs === null) throw new Error('The verbs are not defined'); + this.#userGroupWorkspaceContext?.setFallbackPermissions(verbs); + } + + override render() { + return this.entityType + ? html` + + ` + : nothing; + } + + static override styles = [UmbTextStyles]; +} + +export default UmbUserGroupEntityTypePermissionsElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-group-entity-type-permissions': UmbUserGroupEntityTypePermissionsElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-user-permission-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-user-permission-list.element.ts deleted file mode 100644 index d9ecbdbf19..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-user-permission-list.element.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context-token.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; - -@customElement('umb-user-group-entity-user-permission-list') -export class UmbUserGroupEntityUserPermissionListElement extends UmbLitElement { - @state() - private _fallBackPermissions?: Array; - - @state() - private _groups: Array<{ entityType: string; headline: string }> = []; - - #userGroupWorkspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; - - constructor() { - super(); - - this.#observeEntityUserPermissions(); - - this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => { - this.#userGroupWorkspaceContext = instance; - this.observe( - this.#userGroupWorkspaceContext?.fallbackPermissions, - (fallbackPermissions) => { - this._fallBackPermissions = fallbackPermissions; - }, - 'umbUserGroupEntityUserPermissionsObserver', - ); - }); - } - - #observeEntityUserPermissions() { - this.observe( - umbExtensionsRegistry.byType('entityUserPermission'), - (manifests) => { - const entityTypes = [...new Set(manifests.flatMap((manifest) => manifest.forEntityTypes))]; - this._groups = entityTypes - .map((entityType) => { - return { - entityType, - headline: this.localize.term(`user_permissionsEntityGroup_${entityType}`), - }; - }) - .sort((a, b) => a.headline.localeCompare(b.headline)); - }, - 'umbUserPermissionsObserver', - ); - } - - #onPermissionChange(event: UmbSelectionChangeEvent) { - event.stopPropagation(); - const target = event.target as any; - const verbs = target.allowedVerbs; - if (verbs === undefined || verbs === null) throw new Error('The verbs are not defined'); - this.#userGroupWorkspaceContext?.setFallbackPermissions(verbs); - } - - override render() { - return html` ${this._groups.map((group) => this.#renderPermissionsForEntityType(group))}`; - } - - #renderPermissionsForEntityType(group: { entityType: string; headline: string }) { - return html` -

    ${group.headline}

    - - `; - } - - static override styles = [UmbTextStyles]; -} - -export default UmbUserGroupEntityUserPermissionListElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-user-group-default-permission-list': UmbUserGroupEntityUserPermissionListElement; - } -} 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 701ff856e9..cbbad3824f 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 @@ -12,8 +12,7 @@ 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-user-permission-list.element.js'; -import './components/user-group-granular-permission-list.element.js'; +import './components/user-group-entity-type-permission-groups.element.js'; @customElement('umb-user-group-workspace-editor') export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { @@ -33,7 +32,7 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { private _aliasCanBeChanged?: UmbUserGroupDetailModel['aliasCanBeChanged'] = true; @state() - private _icon: UmbUserGroupDetailModel['icon'] = null; + private _icon?: UmbUserGroupDetailModel['icon']; @state() private _sections: UmbUserGroupDetailModel['sections'] = []; @@ -68,45 +67,44 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { } #observeUserGroup() { - if (!this.#workspaceContext) return; - 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(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( - this.#workspaceContext.aliasCanBeChanged, + this.#workspaceContext?.aliasCanBeChanged, (value) => (this._aliasCanBeChanged = value), '_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?.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), + this.#workspaceContext?.hasAccessToAllLanguages, + (value) => (this._hasAccessToAllLanguages = value ?? false), '_observeHasAccessToAllLanguages', ); this.observe( - this.#workspaceContext.documentRootAccess, - (value) => (this._documentRootAccess = value), + this.#workspaceContext?.documentRootAccess, + (value) => (this._documentRootAccess = value ?? false), '_observeDocumentRootAccess', ); this.observe( - this.#workspaceContext.documentStartNode, + this.#workspaceContext?.documentStartNode, (value) => (this._documentStartNode = value), '_observeDocumentStartNode', ); this.observe( - this.#workspaceContext.mediaRootAccess, - (value) => (this._mediaRootAccess = value), + this.#workspaceContext?.mediaRootAccess, + (value) => (this._mediaRootAccess = value ?? false), '_observeMediaRootAccess', ); this.observe( - this.#workspaceContext.mediaStartNode, + this.#workspaceContext?.mediaStartNode, (value) => (this._mediaStartNode = value), '_observeMediaStartNode', ); @@ -242,20 +240,7 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { ${this.#renderLanguageAccess()} ${this.#renderDocumentAccess()} ${this.#renderMediaAccess()} - -
    - - - - -
    - - -
    - -
    + ${this.#renderPermissionGroups()}
    `; @@ -337,6 +322,10 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { `; } + #renderPermissionGroups() { + return html` `; + } + static override styles = [ UmbTextStyles, css` diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/components/input-entity-user-permission/input-entity-user-permission.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/components/input-entity-user-permission/input-entity-user-permission.element.ts index f6fe7c2b9c..5c654b5ed8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/components/input-entity-user-permission/input-entity-user-permission.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/components/input-entity-user-permission/input-entity-user-permission.element.ts @@ -1,7 +1,7 @@ import type { ManifestEntityUserPermission } from '../../entity-user-permission.extension.js'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { html, customElement, property, state, nothing, ifDefined, css } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, state, ifDefined, css, repeat } from '@umbraco-cms/backoffice/external/lit'; import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import type { UmbUserPermissionVerbElement } from '@umbraco-cms/backoffice/user'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -24,7 +24,7 @@ export class UmbInputEntityUserPermissionElement extends UmbFormControlMixin(Umb allowedVerbs: Array = []; @state() - private _manifests: Array = []; + private _groupedPermissions: Array<[string, ManifestEntityUserPermission[]]> = []; #manifestObserver?: UmbObserverController>; @@ -40,11 +40,18 @@ export class UmbInputEntityUserPermissionElement extends UmbFormControlMixin(Umb this.#manifestObserver?.destroy(); this.#manifestObserver = this.observe( - umbExtensionsRegistry.byType('entityUserPermission'), - (userPermissionManifests) => { - this._manifests = userPermissionManifests.filter((manifest) => - manifest.forEntityTypes.includes(this.entityType), - ); + umbExtensionsRegistry.byTypeAndFilter('entityUserPermission', (manifest) => + manifest.forEntityTypes.includes(this.entityType), + ), + (manifests) => { + // TODO: groupBy is not known by TS yet + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const groupedPermissions = Object.groupBy( + manifests, + (manifest: ManifestEntityUserPermission) => manifest.meta.group, + ) as Record>; + this._groupedPermissions = Object.entries(groupedPermissions); }, 'umbUserPermissionManifestsObserver', ); @@ -73,27 +80,14 @@ export class UmbInputEntityUserPermissionElement extends UmbFormControlMixin(Umb } override render() { - return html`${this.#renderGroupedPermissions(this._manifests)} `; - } - - #renderGroupedPermissions(permissionManifests: Array) { - // TODO: groupBy is not known by TS yet - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const groupedPermissions = Object.groupBy( - permissionManifests, - (manifest: ManifestEntityUserPermission) => manifest.meta.group, - ) as Record>; - return html` - ${Object.entries(groupedPermissions).map( - ([group, manifests]) => html` - ${group !== 'undefined' - ? html`
    ${group}
    ` - : nothing} -
    ${manifests.map((manifest) => html` ${this.#renderPermission(manifest)} `)}
    - `, - )} - `; + return repeat(this._groupedPermissions, ([group, manifests]) => { + const headline = group !== 'undefined' ? `#actionCategories_${group}` : `#actionCategories_general`; + return html` + +
    ${repeat(manifests, (manifest) => html` ${this.#renderPermission(manifest)} `)}
    +
    + `; + }); } #renderPermission(manifest: ManifestEntityUserPermission) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts index 974200ac95..ffbbf3fc47 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts @@ -16,5 +16,6 @@ export const UMB_ENTITY_USER_PERMISSION_MODAL = new UmbModalToken< >('Umb.Modal.EntityUserPermissionSettings', { modal: { type: 'sidebar', + size: 'medium', }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/user-granular-permission.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/user-granular-permission.extension.ts index 70801b97b3..5023055136 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/user-granular-permission.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/user-granular-permission.extension.ts @@ -2,6 +2,7 @@ import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api'; export interface ManifestGranularUserPermission extends ManifestElement { type: 'userGranularPermission'; + forEntityTypes?: Array; meta: MetaGranularUserPermission; } From 58068d1aa7dff8b353d42f543ed6d28c9a9e42eb Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 21 Oct 2025 14:41:29 +0200 Subject: [PATCH 25/28] Rendering: Explicitly contextualize variation context for language fallback (closes #20350) (#20587) Expliticly contextualize variation context for language fallback rendering --- .../PublishedValueFallback.cs | 72 ++++++++++----- .../PublishedContentFallbackTests.cs | 70 +++++++++++++- ...stElementLevelVariationTests.Publishing.cs | 91 +++++++++++++++++++ 3 files changed, 209 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs index 7265b763b8..a200972cf7 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs @@ -317,7 +317,7 @@ public class PublishedValueFallback : IPublishedValueFallback } var culture2 = language2.IsoCode; - T? culture2Value = getValue(culture2, segment); + T? culture2Value = TryGetExplicitlyContextualizedValue(getValue, culture2, segment); if (culture2Value != null) { value = culture2Value; @@ -329,25 +329,26 @@ public class PublishedValueFallback : IPublishedValueFallback } private bool TryGetValueWithDefaultLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) - { - value = default; - - if (culture.IsNullOrWhiteSpace()) - { - return false; - } - - string? defaultCulture = _localizationService?.GetDefaultLanguageIsoCode(); - if (culture.InvariantEquals(defaultCulture) == false && property.HasValue(defaultCulture, segment)) - { - value = property.Value(this, defaultCulture, segment); - return true; - } - - return false; - } + => TryGetValueWithDefaultLanguageFallback( + (actualCulture, actualSegment) + => property.HasValue(actualCulture, actualSegment) + ? property.Value(this, actualCulture, actualSegment) + : default, + culture, + segment, + out value); private bool TryGetValueWithDefaultLanguageFallback(IPublishedElement element, string alias, string? culture, string? segment, out T? value) + => TryGetValueWithDefaultLanguageFallback( + (actualCulture, actualSegment) + => element.HasValue(alias, actualCulture, actualSegment) + ? element.Value(this, alias, actualCulture, actualSegment) + : default, + culture, + segment, + out value); + + private bool TryGetValueWithDefaultLanguageFallback(TryGetValueForCultureAndSegment getValue, string? culture, string? segment, out T? value) { value = default; @@ -356,14 +357,39 @@ public class PublishedValueFallback : IPublishedValueFallback return false; } - string? defaultCulture = _localizationService?.GetDefaultLanguageIsoCode(); - if (culture.InvariantEquals(defaultCulture) == false && element.HasValue(alias, defaultCulture, segment)) + var defaultCulture = _localizationService?.GetDefaultLanguageIsoCode(); + if (defaultCulture.IsNullOrWhiteSpace()) { - value = element.Value(this, alias, defaultCulture, segment); - return true; + return false; } - return false; + if (culture.InvariantEquals(defaultCulture)) + { + return false; + } + + T? fallbackValue = TryGetExplicitlyContextualizedValue(getValue, defaultCulture, segment); + if (fallbackValue == null) + { + return false; + } + + value = fallbackValue; + return true; + } + + private T? TryGetExplicitlyContextualizedValue(TryGetValueForCultureAndSegment getValue, string culture, string? segment) + { + VariationContext? current = _variationContextAccessor.VariationContext; + try + { + _variationContextAccessor.VariationContext = new VariationContext(culture, segment); + return getValue(culture, segment); + } + finally + { + _variationContextAccessor.VariationContext = current; + } } private delegate T? TryGetValueForCultureAndSegment(string actualCulture, string? actualSegment); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs index f051aa1557..6739487317 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs @@ -35,6 +35,8 @@ public class PublishedContentFallbackTests : UmbracoIntegrationTest private IApiContentBuilder ApiContentBuilder => GetRequiredService(); + private ILanguageService LanguageService => GetRequiredService(); + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder .AddUmbracoHybridCache() @@ -98,6 +100,36 @@ public class PublishedContentFallbackTests : UmbracoIntegrationTest Assert.AreEqual(invariantTitle, invariantValue); } + [TestCase("Danish title", true)] + [TestCase("Danish title", false)] + [TestCase(null, true)] + [TestCase(null, false)] + public async Task Property_Value_Can_Perform_Explicit_Language_Fallback(string? danishTitle, bool performFallbackToDefaultLanguage) + { + var danishLanguage = new Language("da-DK", "Danish") + { + FallbackIsoCode = "en-US" + }; + await LanguageService.CreateAsync(danishLanguage, Constants.Security.SuperUserKey); + + UmbracoContextFactory.EnsureUmbracoContext(); + + const string englishTitle = "English title"; + var publishedContent = await SetupCultureVariantContentAsync(englishTitle, danishTitle); + + VariationContextAccessor.VariationContext = new VariationContext(culture: "da-DK", segment: null); + var danishValue = publishedContent.Value(PublishedValueFallback, "title"); + Assert.AreEqual(danishTitle ?? string.Empty, danishValue); + + var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage; + var fallbackValue = publishedContent.Value(PublishedValueFallback, "title", fallback: fallback); + Assert.AreEqual(danishTitle ?? englishTitle, fallbackValue); + + VariationContextAccessor.VariationContext = new VariationContext(culture: "en-US", segment: null); + var englishValue = publishedContent.Value(PublishedValueFallback, "title"); + Assert.AreEqual(englishTitle, englishValue); + } + private async Task SetupSegmentedContentAsync(string? invariantTitle, string? segmentedTitle) { var contentType = new ContentTypeBuilder() @@ -124,11 +156,47 @@ public class PublishedContentFallbackTests : UmbracoIntegrationTest ContentService.Save(content); ContentService.Publish(content, ["*"]); + return GetPublishedContent(content.Key); + } + + private async Task SetupCultureVariantContentAsync(string englishTitle, string? danishTitle) + { + var contentType = new ContentTypeBuilder() + .WithAlias("theContentType") + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Culture) + .Done() + .WithAllowAsRoot(true) + .Build(); + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + + var content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", "EN") + .WithCultureName("da-DK", "DA") + .WithName("Content") + .Build(); + content.SetValue("title", englishTitle, culture: "en-US"); + content.SetValue("title", danishTitle, culture: "da-DK"); + ContentService.Save(content); + ContentService.Publish(content, ["en-US", "da-DK"]); + + return GetPublishedContent(content.Key); + } + + private IPublishedContent GetPublishedContent(Guid key) + { ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]); UmbracoContextAccessor.Clear(); var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext; - var publishedContent = umbracoContext.Content.GetById(content.Key); + var publishedContent = umbracoContext.Content.GetById(key); Assert.IsNotNull(publishedContent); return publishedContent; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs index 48e225045d..db57a313e8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs @@ -1932,4 +1932,95 @@ internal partial class BlockListElementLevelVariationTests Assert.AreEqual(expectedPickedContent.Key, actualPickedPublishedContent.Key); } } + + [TestCase(ContentVariation.Culture, false)] + [TestCase(ContentVariation.Culture, true)] + [TestCase(ContentVariation.Nothing, false)] + [TestCase(ContentVariation.Nothing, true)] + public async Task Can_Perform_Language_Fallback(ContentVariation elementTypeVariation, bool performFallbackToDefaultLanguage) + { + var daDkLanguage = await LanguageService.GetAsync("da-DK"); + Assert.IsNotNull(daDkLanguage); + daDkLanguage.FallbackIsoCode = "en-US"; + var saveLanguageResult = await LanguageService.UpdateAsync(daDkLanguage, Constants.Security.SuperUserKey); + Assert.IsTrue(saveLanguageResult.Success); + + daDkLanguage = await LanguageService.GetAsync("da-DK"); + Assert.AreEqual("en-US", daDkLanguage?.FallbackIsoCode); + + var elementType = CreateElementType(elementTypeVariation); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "English invariantText content value" }, + new() { Alias = "variantText", Value = "English variantText content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "English invariantText settings value" }, + new() { Alias = "variantText", Value = "English variantText settings value" } + }, + "en-US", + null) + }, + true); + + AssertPropertyValuesWithFallback("en-US", + "English invariantText content value", "English variantText content value", + "English invariantText settings value", "English variantText settings value"); + + AssetEmptyPropertyValues("da-DK"); + + AssertPropertyValuesWithFallback("da-DK", + "English invariantText content value", "English variantText content value", + "English invariantText settings value", "English variantText settings value"); + + void AssertPropertyValuesWithFallback(string culture, + string expectedInvariantContentValue, string expectedVariantContentValue, + string expectedInvariantSettingsValue, string expectedVariantSettingsValue) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage; + + var publishedValueFallback = GetRequiredService(); + var value = publishedContent.Value(publishedValueFallback, "blocks", fallback: fallback); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantSettingsValue, blockListItem.Settings.Value("invariantText")); + Assert.AreEqual(expectedVariantSettingsValue, blockListItem.Settings.Value("variantText")); + }); + } + + void AssetEmptyPropertyValues(string culture) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.NotNull(value); + Assert.IsEmpty(value); + } + } } From de5a9ca5af565d4583d6ada7a4cba753d643fe23 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:20:17 +0200 Subject: [PATCH 26/28] Preview: Replaces WebSocket with the SignalR library to improve connectivity in the preview window (#20585) * feat: replaces manual WebSocket with the actual SignalR library on the preview context * feat: informs the developer what went wrong in preview mode * feat: awaits the stop connection before proceeding * feat: ensures no existing connection exists --- .../src/apps/preview/preview.context.ts | 90 ++++++++++++------- .../src/assets/lang/da.ts | 5 +- .../src/assets/lang/en.ts | 2 + 3 files changed, 65 insertions(+), 32 deletions(-) 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 75de96a38c..2acbd754de 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 @@ -7,6 +7,9 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document'; import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'; 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'; @@ -31,7 +34,7 @@ export class UmbPreviewContext extends UmbContextBase { #culture?: string | null; #segment?: string | null; #serverUrl: string = ''; - #webSocket?: WebSocket; + #connection?: HubConnection; #iframeReady = new UmbBooleanState(false); public readonly iframeReady = this.#iframeReady.asObservable(); @@ -41,12 +44,13 @@ export class UmbPreviewContext extends UmbContextBase { #documentPreviewRepository = new UmbDocumentPreviewRepository(this); + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + #localize = new UmbLocalizationController(this); + constructor(host: UmbControllerHost) { super(host, UMB_PREVIEW_CONTEXT); this.consumeContext(UMB_SERVER_CONTEXT, (instance) => { - this.#serverUrl = instance?.getServerUrl() ?? ''; - const params = new URLSearchParams(window.location.search); this.#unique = params.get('id'); @@ -58,37 +62,62 @@ export class UmbPreviewContext extends UmbContextBase { return; } - this.#setPreviewUrl(); + const serverUrl = instance?.getServerUrl(); + + if (!serverUrl) { + console.error('No server URL found in context'); + return; + } + + this.#serverUrl = serverUrl; + + this.#setPreviewUrl({ serverUrl }); + + this.#initHubConnection(serverUrl); + }); + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { + this.#notificationContext = instance; }); } - #configureWebSocket() { - if (this.#webSocket && this.#webSocket.readyState < 2) return; + async #initHubConnection(serverUrl: string) { + const previewHubUrl = `${serverUrl}/umbraco/PreviewHub`; - const url = `${this.#serverUrl.replace('https://', 'wss://')}/umbraco/PreviewHub`; + // Make sure that no previous connection exists. + if (this.#connection) { + await this.#connection.stop(); + this.#connection = undefined; + } - this.#webSocket = new WebSocket(url); + this.#connection = new HubConnectionBuilder().withUrl(previewHubUrl).build(); - this.#webSocket.addEventListener('open', () => { - // NOTE: SignalR protocol handshake; it requires a terminating control character. - const endChar = String.fromCharCode(30); - this.#webSocket?.send(`{"protocol":"json","version":1}${endChar}`); - }); - - this.#webSocket.addEventListener('message', (event: MessageEvent) => { - if (!event?.data) return; - - // NOTE: Strip the terminating control character, (from SignalR). - const data = event.data.substring(0, event.data.length - 1); - const json = JSON.parse(data) as { type: number; target: string; arguments: Array }; - - if (json.type === 1 && json.target === 'refreshed') { - const pageId = json.arguments?.[0]; - if (pageId === this.#unique) { - this.#setPreviewUrl({ rnd: Math.random() }); - } + this.#connection.on('refreshed', (payload) => { + if (payload === this.#unique) { + this.#setPreviewUrl({ rnd: Math.random() }); } }); + + this.#connection.onclose(() => { + this.#notificationContext?.peek('warning', { + data: { + headline: this.#localize.term('general_preview'), + message: this.#localize.term('preview_connectionLost'), + }, + }); + }); + + try { + await this.#connection.start(); + } catch (error) { + console.error('The SignalR connection could not be established', error); + this.#notificationContext?.peek('warning', { + data: { + headline: this.#localize.term('general_preview'), + message: this.#localize.term('preview_connectionFailed'), + }, + }); + } } async #getPublishedUrl(): Promise { @@ -144,7 +173,7 @@ export class UmbPreviewContext extends UmbContextBase { params.delete(segmentParam); } - const previewUrl = new URL(url.pathname + '?' + params.toString(), host); + const previewUrl = new URL(`${url.pathname}?${params.toString()}`, host); const previewUrlString = previewUrl.toString(); this.#previewUrl.setValue(previewUrlString); @@ -180,9 +209,9 @@ export class UmbPreviewContext extends UmbContextBase { await this.#documentPreviewRepository.exit(); } - if (this.#webSocket) { - this.#webSocket.close(); - this.#webSocket = undefined; + if (this.#connection) { + await this.#connection.stop(); + this.#connection = undefined; } let url = await this.#getPublishedUrl(); @@ -202,7 +231,6 @@ export class UmbPreviewContext extends UmbContextBase { iframeLoaded(iframe: HTMLIFrameElement) { if (!iframe) return; - this.#configureWebSocket(); this.#iframeReady.setValue(true); } diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts index 2b08aaf2f5..02b918c910 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts @@ -2603,13 +2603,16 @@ export default { returnToPreviewHeadline: 'Forhåndsvisning af indholdet?', returnToPreviewDescription: 'Du har afslutet forhåndsvisning, vil du starte forhåndsvisning igen for at\n se seneste gemte version af indholdet?\n ', + returnToPreviewAcceptButton: 'Start forhåndsvisning igen', returnToPreviewDeclineButton: 'Se udgivet indhold', viewPublishedContentHeadline: 'Se udgivet indhold?', viewPublishedContentDescription: 'Du er i forhåndsvisning, vil du afslutte for at se den udgivet\n version?\n ', viewPublishedContentAcceptButton: 'Se udgivet version', viewPublishedContentDeclineButton: 'Forbliv i forhåndsvisning', - returnToPreviewAcceptButton: 'Preview latest version', + connectionFailed: + 'Kunne ikke etablere forbindelse til serveren, forhåndsvisning af liveopdateringer vil ikke fungere.', + connectionLost: 'Forbindelse til serveren mistet, forhåndsvisning af liveopdateringer vil ikke fungere.', }, permissions: { FolderCreation: 'Mappeoprettelse', 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 d91f0234bb..68370c54c1 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2749,6 +2749,8 @@ export default { 'You are in Preview Mode, do you want exit in order to view the published version of your website?', viewPublishedContentAcceptButton: 'View published version', viewPublishedContentDeclineButton: 'Stay in preview mode', + connectionFailed: 'Could not establish a connection to the server, preview live updates will not work.', + connectionLost: 'Connection to the server lost, preview live updates will not work.', }, permissions: { FolderCreation: 'Folder creation', From 5f0122e18bc62eaef14596eecc81f095ab77334b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 21 Oct 2025 15:24:52 +0200 Subject: [PATCH 27/28] Make Create Actions open as dialogs (part 2) (#20523) * open script create options as a dialog instead of sidebar * align and simplify the blueprint dialog options * debounce loadCollection calls * update document collection context * update media collection context * Updated tests * Bumped test helpers * Revert "debounce loadCollection calls" This reverts commit 1c15dda08d2058aeffe30a5173bdfbfd47dfe0ce. * Revert "update document collection context" This reverts commit 47d74a8f5d220a53b3bcd4857a9617de51124fbf. * Revert "update media collection context" This reverts commit f2eb1f22c10567666771f43a09c3715a1e1ba9d1. * align create dialog texts * fix indention * Bumped version of test helpers --------- Co-authored-by: Andreas Zerbst Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> --- .../src/assets/lang/en.ts | 18 +++--- ...create-option-action-list-modal.element.ts | 2 +- .../entity-create-option-action/manifests.ts | 2 +- .../entity-actions/create/folder/manifests.ts | 2 +- .../entity-actions/create/modal/constants.ts | 3 +- ...-blueprint-options-create-modal.element.ts | 56 ++++++++++--------- .../entity-actions/create/folder/manifests.ts | 2 +- .../entity-actions/create/folder/manifests.ts | 2 +- ...rtial-view-create-options-modal.element.ts | 12 +++- .../create/options-modal/index.ts | 3 +- .../script-create-options-modal.element.ts | 21 +++---- .../entity-actions/create/manifests.ts | 2 +- .../package-lock.json | 16 +++--- .../Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../DocumentBlueprint.spec.ts | 4 ++ 15 files changed, 82 insertions(+), 65 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 68370c54c1..f9b805b8bd 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -472,16 +472,16 @@ export default { "Defines a re-usable set of properties that can be included in the definition of multiple other Document Types. For example, a set of 'Common Page Settings'.", folder: 'Folder', folderDescription: 'Used to organise items and other folders. Keep items structured and easy to access.', - newFolder: 'New folder', - newDataType: 'New Data Type', + newFolder: 'Folder', + newDataType: 'Data Type', newDataTypeDescription: 'Used to define a configuration for a Property Type on a Content Type.', - newJavascriptFile: 'New JavaScript file', - newEmptyPartialView: 'New empty partial view', - newPartialViewMacro: 'New partial view macro', - newPartialViewFromSnippet: 'New partial view from snippet', - newPartialViewMacroFromSnippet: 'New partial view macro from snippet', - newPartialViewMacroNoMacro: 'New partial view macro (without macro)', - newStyleSheetFile: 'New Stylesheet', + newJavascriptFile: 'JavaScript file', + newEmptyPartialView: 'Empty partial view', + newPartialViewMacro: 'Partial view macro', + newPartialViewFromSnippet: 'Partial view from snippet', + newPartialViewMacroFromSnippet: 'Partial view macro from snippet', + newPartialViewMacroNoMacro: 'Partial view macro (without macro)', + newStyleSheetFile: 'Stylesheet file', }, dashboard: { browser: 'Browse your website', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts index ab0654c5de..176e643496 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/modal/entity-create-option-action-list-modal.element.ts @@ -132,7 +132,7 @@ export class UmbEntityCreateOptionActionListModalElement extends UmbModalBaseEle return html` = meta: { icon: 'icon-folder', label: '#create_folder', - description: '#create_folderDescription', + additionalOptions: true, }, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/folder/manifests.ts index dc54d290a8..d9abd0ac95 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/folder/manifests.ts @@ -12,7 +12,7 @@ export const manifests: Array = meta: { icon: 'icon-folder', label: '#create_folder', - description: '#create_folderDescription', + additionalOptions: true, folderRepositoryAlias: UMB_DATA_TYPE_FOLDER_REPOSITORY_ALIAS, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/constants.ts index 6d261ca4c4..4cb54e036c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/constants.ts @@ -14,7 +14,6 @@ export const UMB_DOCUMENT_BLUEPRINT_OPTIONS_CREATE_MODAL = new UmbModalToken< UmbDocumentBlueprintOptionsCreateModalValue >('Umb.Modal.DocumentBlueprintOptionsCreate', { modal: { - type: 'sidebar', - size: 'small', + type: 'dialog', }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/document-blueprint-options-create-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/document-blueprint-options-create-modal.element.ts index 41bb58dc45..93e188b442 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/document-blueprint-options-create-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/document-blueprint-options-create-modal.element.ts @@ -8,9 +8,13 @@ import type { } from './constants.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { html, customElement, css, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { type UmbSelectedEvent, UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event'; -import { UmbCreateFolderEntityAction, type UmbTreeElement } from '@umbraco-cms/backoffice/tree'; +import { UmbModalBaseElement, umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbCreateFolderEntityAction } from '@umbraco-cms/backoffice/tree'; +import { + UMB_DOCUMENT_TYPE_PICKER_MODAL, + type UmbDocumentTypeTreeItemModel, +} from '@umbraco-cms/backoffice/document-type'; @customElement('umb-document-blueprint-options-create-modal') export class UmbDocumentBlueprintOptionsCreateModalElement extends UmbModalBaseElement< @@ -58,40 +62,42 @@ export class UmbDocumentBlueprintOptionsCreateModalElement extends UmbModalBaseE .catch(() => {}); } - #onSelected(event: UmbSelectedEvent) { + async #onCreateBlueprintClick(event: PointerEvent) { event.stopPropagation(); - const element = event.target as UmbTreeElement; - this.value = { documentTypeUnique: element.getSelection()[0] }; + const value = await umbOpenModal(this, UMB_DOCUMENT_TYPE_PICKER_MODAL, { + data: { + hideTreeRoot: true, + pickableFilter: (item: UmbDocumentTypeTreeItemModel) => item.isElement == false, + }, + }); + + const selection = value.selection.filter((x) => x !== null); + this.value = { documentTypeUnique: selection[0] }; this.modalContext?.dispatchEvent(new UmbSelectionChangeEvent()); this._submitModal(); } override render() { return html` - - - - - - - - - Select the Document Type you want to make a Document Blueprint for - - item.isElement == false, - }} - @selected=${this.#onSelected}> - + + + + + + + - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/folder/manifests.ts index 95ba1046b7..a7d9d0365f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/folder/manifests.ts @@ -15,7 +15,7 @@ export const manifests: Array = meta: { icon: 'icon-folder', label: '#create_folder', - description: '#create_folderDescription', + additionalOptions: true, folderRepositoryAlias: UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/folder/manifests.ts index 101d70fc08..90994ed1c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/folder/manifests.ts @@ -12,7 +12,7 @@ export const manifests: Array = meta: { icon: 'icon-folder', label: '#create_folder', - description: '#create_folderDescription', + additionalOptions: true, folderRepositoryAlias: UMB_MEDIA_TYPE_FOLDER_REPOSITORY_ALIAS, }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts index 5f7b27b28a..0bde152792 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts @@ -1,7 +1,7 @@ import { UMB_PARTIAL_VIEW_FROM_SNIPPET_MODAL } from '../snippet-modal/index.js'; import { UMB_PARTIAL_VIEW_FOLDER_REPOSITORY_ALIAS } from '../../../constants.js'; import type { UmbPartialViewCreateOptionsModalData } from './index.js'; -import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, css } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement, umbOpenModal } from '@umbraco-cms/backoffice/modal'; import { UmbCreateFolderEntityAction } from '@umbraco-cms/backoffice/tree'; @@ -62,7 +62,7 @@ export class UmbPartialViewCreateOptionsModalElement extends UmbModalBaseElement override render() { return html` - + `; } + + static override styles = [ + css` + uui-dialog-layout { + --uui-menu-item-flat-structure: 1; + } + `, + ]; } export default UmbPartialViewCreateOptionsModalElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/index.ts index bb3f144fdd..a15bf4f8c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/index.ts @@ -5,8 +5,7 @@ export const UMB_SCRIPT_CREATE_OPTIONS_MODAL = new UmbModalToken - + + - - } - + + - - } - - + + - + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts index 1b13642c84..fae97fe98c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts @@ -46,7 +46,7 @@ const entityCreateOptionActions: Array = [ meta: { icon: 'icon-folder', label: '#create_folder', - description: '#create_folderDescription', + additionalOptions: true, folderRepositoryAlias: UMB_STYLESHEET_FOLDER_REPOSITORY_ALIAS, }, }, diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 87ed32ef3a..4b057b1d03 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.4", + "@umbraco/playwright-testhelpers": "^17.0.0-beta.7", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -58,21 +58,21 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.40", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.40.tgz", - "integrity": "sha512-Yqojp/0akRgXsnjg18+MjMdkRvFrmlUNbfITgZ3d1h/PIRbWXPNKY1YAfZmdUv+g1SRSHrbIRpPPtSy+gNOjHw==", + "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==", "license": "MIT", "dependencies": { "camelize": "^1.0.1" } }, "node_modules/@umbraco/playwright-testhelpers": { - "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==", + "version": "17.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.0-beta.7.tgz", + "integrity": "sha512-5fhmVVSpJkH6Inx8nA9qqqvZzYuPdDxJdQF2IzY0oSf8C0eti+TJ2BKrYfTLmZTfVqmHUas72BMGser5pfpl9A==", "license": "MIT", "dependencies": { - "@umbraco/json-models-builders": "2.0.40", + "@umbraco/json-models-builders": "2.0.41", "node-fetch": "^2.6.7" } }, diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index f1a7b85fa4..013bfdd438 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.4", + "@umbraco/playwright-testhelpers": "^17.0.0-beta.7", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts index fdcef65d63..f9e7ce5a72 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts @@ -23,7 +23,9 @@ test('can create a document blueprint from the settings menu', {tag: '@smoke'}, // Act await umbracoUi.documentBlueprint.clickActionsMenuAtRoot(); await umbracoUi.documentBlueprint.clickCreateActionMenuOption(); + await umbracoUi.documentBlueprint.clickCreateNewDocumentBlueprintButton(); await umbracoUi.documentBlueprint.clickTextButtonWithName(documentTypeName); + await umbracoUi.documentBlueprint.clickChooseButton(); await umbracoUi.documentBlueprint.enterDocumentBlueprintName(documentBlueprintName); await umbracoUi.documentBlueprint.clickSaveButton(); @@ -108,7 +110,9 @@ test('can create a variant document blueprint', {tag: '@release'}, async ({umbra // Act await umbracoUi.documentBlueprint.clickActionsMenuAtRoot(); await umbracoUi.documentBlueprint.clickCreateActionMenuOption(); + await umbracoUi.documentBlueprint.clickCreateNewDocumentBlueprintButton(); await umbracoUi.documentBlueprint.clickTextButtonWithName(documentTypeName); + await umbracoUi.documentBlueprint.clickChooseButton(); await umbracoUi.documentBlueprint.enterDocumentBlueprintName(documentBlueprintName); await umbracoUi.documentBlueprint.clickSaveButton(); From f0ac7b4a50bd7625c391e4c3566e3cfee2ce928b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 21 Oct 2025 15:51:28 +0200 Subject: [PATCH 28/28] Document/Media Recycle Bin: Add 'Trashed' state to info workspace view (#20581) * Add 'Trashed' state to document workspace view Introduces a new 'Trashed' label and tag for documents in the workspace view. Updates localization to include the 'Trashed' term for improved clarity when displaying trashed documents. * Show trashed state in media workspace info view --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 1 + .../info/document-workspace-view-info.element.ts | 6 ++++++ .../info/media-workspace-view-info.element.ts | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) 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 f9b805b8bd..7ea2feb2bb 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -294,6 +294,7 @@ export default { titleOptional: 'Title (optional)', altTextOptional: 'Alternative text (optional)', captionTextOptional: 'Caption (optional)', + trashed: 'Trashed', type: 'Type', unpublish: 'Unpublish', unpublished: 'Unpublished', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts index d703ded970..12e77046e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts @@ -179,6 +179,12 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { `; } + case DocumentVariantStateModel.TRASHED: + return html` + + ${this.localize.term('content_trashed')} + + `; default: return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts index 77fbe7839d..630631bed3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts @@ -129,7 +129,7 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { #renderGeneralSection() { return html` - ${this.#renderCreateDate()} ${this.#renderUpdateDate()} + ${this.#renderTrashState()} ${this.#renderCreateDate()} ${this.#renderUpdateDate()}
    Media Type + + + ${this.localize.term('content_trashed')} + + +
    + `; + } + #renderCreateDate() { if (!this._createDate) return nothing; return html`