From be116436d93eea2fe92a59361bbaa16f22a4a8cf Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 19 Nov 2025 15:35:48 +0100 Subject: [PATCH 1/6] Migrations: Handles rich text blocks created with TinyMCE in convert local links migration and refreshes internal datatype cache following migration requiring cache rebuild (closes #20885) (#20887) Handles rich text blocks created with TinyMCE in convert local links migration. Refreshes internal datatype cache following migration requiring cache rebuild. # Conflicts: # src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs --- .../Models/Blocks/RichTextBlockValue.cs | 7 ++++ .../IPublishedContentTypeFactory.cs | 6 ++++ .../PublishedContentTypeFactory.cs | 16 +++++++++ .../Migrations/MigrationPlanExecutor.cs | 33 +++++++++++++++++++ .../Migrations/AdvancedMigrationTests.cs | 5 ++- .../RichTextPropertyEditorHelperTests.cs | 16 ++++++--- .../Migrations/MigrationPlanTests.cs | 4 ++- 7 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs index 6cbab55693..3053e02d8b 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs @@ -23,4 +23,11 @@ public class RichTextBlockValue : BlockValue /// [JsonIgnore] public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.RichText; + + /// +#pragma warning disable CS0672 // Member overrides obsolete member +#pragma warning disable CS0618 // Type or member is obsolete + public override bool SupportsBlockLayoutAlias(string alias) => base.SupportsBlockLayoutAlias(alias) || alias.Equals("Umbraco.TinyMCE"); +#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0672 // Member overrides obsolete member } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index 009666aab5..9a5a14d7d4 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -53,6 +53,12 @@ public interface IPublishedContentTypeFactory /// PublishedDataType GetDataType(int id); + /// + /// Clears the internal data type cache. + /// + void ClearDataTypeCache() + { } + /// /// Notifies the factory of datatype changes. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 32fab1b539..7ea8779718 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -65,6 +65,22 @@ public class PublishedContentTypeFactory : IPublishedContentTypeFactory return dataType; } + /// + public void ClearDataTypeCache() + { + if (_publishedDataTypes is null) + { + // Not initialized yet, so skip and avoid lock + return; + } + + lock (_publishedDataTypesLocker) + { + // Clear cache (and let it lazy initialize again later) + _publishedDataTypes = null; + } + } + /// public void NotifyDataTypeChanges(params int[] ids) { diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index 878d0e4d3b..45b1ebfbd4 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; @@ -51,9 +52,12 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor private readonly DistributedCache _distributedCache; private readonly IScopeAccessor _scopeAccessor; private readonly ICoreScopeProvider _scopeProvider; + private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; + private bool _rebuildCache; private bool _invalidateBackofficeUserAccess; + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")] public MigrationPlanExecutor( ICoreScopeProvider scopeProvider, IScopeAccessor scopeAccessor, @@ -65,6 +69,33 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor IKeyValueService keyValueService, IServiceScopeFactory serviceScopeFactory, AppCaches appCaches) + : this( + scopeProvider, + scopeAccessor, + loggerFactory, + migrationBuilder, + databaseFactory, + databaseCacheRebuilder, + distributedCache, + keyValueService, + serviceScopeFactory, + appCaches, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public MigrationPlanExecutor( + ICoreScopeProvider scopeProvider, + IScopeAccessor scopeAccessor, + ILoggerFactory loggerFactory, + IMigrationBuilder migrationBuilder, + IUmbracoDatabaseFactory databaseFactory, + IDatabaseCacheRebuilder databaseCacheRebuilder, + DistributedCache distributedCache, + IKeyValueService keyValueService, + IServiceScopeFactory serviceScopeFactory, + AppCaches appCaches, + IPublishedContentTypeFactory publishedContentTypeFactory) { _scopeProvider = scopeProvider; _scopeAccessor = scopeAccessor; @@ -76,6 +107,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor _serviceScopeFactory = serviceScopeFactory; _appCaches = appCaches; _distributedCache = distributedCache; + _publishedContentTypeFactory = publishedContentTypeFactory; _logger = _loggerFactory.CreateLogger(); } @@ -301,6 +333,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor _appCaches.IsolatedCaches.ClearAllCaches(); await _databaseCacheRebuilder.RebuildAsync(false); _distributedCache.RefreshAllPublishedSnapshot(); + _publishedContentTypeFactory.ClearDataTypeCache(); } private async Task RevokeBackofficeTokens() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index b6fe2f48ed..444f94c218 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -38,6 +39,7 @@ internal sealed class AdvancedMigrationTests : UmbracoIntegrationTest private IServiceScopeFactory ServiceScopeFactory => GetRequiredService(); private DistributedCache DistributedCache => GetRequiredService(); private IDatabaseCacheRebuilder DatabaseCacheRebuilder => GetRequiredService(); + private IPublishedContentTypeFactory PublishedContentTypeFactory => GetRequiredService(); private IMigrationPlanExecutor MigrationPlanExecutor => new MigrationPlanExecutor( CoreScopeProvider, ScopeAccessor, @@ -48,7 +50,8 @@ internal sealed class AdvancedMigrationTests : UmbracoIntegrationTest DistributedCache, Mock.Of(), ServiceScopeFactory, - AppCaches.NoCache); + AppCaches.NoCache, + PublishedContentTypeFactory); [Test] public async Task CreateTableOfTDtoAsync() diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs index 892e08cdd2..212d13687f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs @@ -110,15 +110,16 @@ public class RichTextPropertyEditorHelperTests Assert.IsNull(value.Blocks); } - [Test] - public void Can_Parse_Blocks_With_Both_Content_And_Settings() + [TestCase(Constants.PropertyEditors.Aliases.RichText)] + [TestCase("Umbraco.TinyMCE")] + public void Can_Parse_Blocks_With_Both_Content_And_Settings(string propertyEditorAlias) { - const string input = """ + string input = """ { "markup": "

this is some markup

", "blocks": { "layout": { - "Umbraco.RichText": [{ + "[PropertyEditorAlias]": [{ "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", "settingsKey": "d2eeef66-4111-42f4-a164-7a523eaffbc2" } @@ -143,6 +144,7 @@ public class RichTextPropertyEditorHelperTests } } """; + input = input.Replace("[PropertyEditorAlias]", propertyEditorAlias); var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); @@ -180,6 +182,12 @@ public class RichTextPropertyEditorHelperTests Assert.AreEqual("settingsPropertyAlias", settingsProperties.First().Alias); Assert.AreEqual("A settings property value", settingsProperties.First().Value); }); + + Assert.IsTrue(value.Blocks.Layout.ContainsKey(Constants.PropertyEditors.Aliases.RichText)); + var layout = value.Blocks.Layout[Constants.PropertyEditors.Aliases.RichText]; + Assert.AreEqual(1, layout.Count()); + Assert.AreEqual(Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"), layout.First().ContentKey); + Assert.AreEqual(Guid.Parse("d2eeef66-4111-42f4-a164-7a523eaffbc2"), layout.First().SettingsKey); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index 261d41128d..a38c342945 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -12,6 +12,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; @@ -85,7 +86,8 @@ public class MigrationPlanTests distributedCache, Mock.Of(), Mock.Of(), - appCaches); + appCaches, + Mock.Of()); var plan = new MigrationPlan("default") .From(string.Empty) From d7231c5435ec8b39b1417a0d7f4c4c23bcf20b79 Mon Sep 17 00:00:00 2001 From: Callum Whyte Date: Mon, 24 Nov 2025 00:53:38 +0000 Subject: [PATCH 2/6] Preserve existing Examine FieldDefinitionCollection if it already exists (#20931) * Preserve existing Examine FieldDefinitionCollection if it already exists (#20267) * Fix missing bracket * Minor tidy/addition of comments; addition of unit tests. --------- Co-authored-by: Andy Butland (cherry picked from commit 908974c6acedec7c3a21f7dd3646bd95f0a51aea) --- .../ConfigureIndexOptions.cs | 15 +++- .../UmbracoFieldDefinitionCollection.cs | 18 +++++ .../UmbracoFieldDefinitionCollectionTests.cs | 75 +++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollectionTests.cs diff --git a/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs b/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs index 2b5bc771f1..1dd2ed3e25 100644 --- a/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs +++ b/src/Umbraco.Examine.Lucene/DependencyInjection/ConfigureIndexOptions.cs @@ -10,7 +10,7 @@ using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection; /// -/// Configures the index options to construct the Examine indexes +/// Configures the index options to construct the Examine indexes. /// public sealed class ConfigureIndexOptions : IConfigureNamedOptions { @@ -18,6 +18,9 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions + /// Initializes a new instance of the class. + /// public ConfigureIndexOptions( IUmbracoIndexConfig umbracoIndexConfig, IOptions settings, @@ -28,24 +31,27 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions public void Configure(string? name, LuceneDirectoryIndexOptions options) { + // When creating FieldDefinitions with Umbraco defaults, pass in any already defined to avoid overwriting + // those added via a package or custom code. switch (name) { case Constants.UmbracoIndexes.InternalIndexName: options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); options.Validator = _umbracoIndexConfig.GetContentValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions); break; case Constants.UmbracoIndexes.ExternalIndexName: options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion); options.Validator = _umbracoIndexConfig.GetPublishedContentValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions); break; case Constants.UmbracoIndexes.MembersIndexName: options.Analyzer = new CultureInvariantWhitespaceAnalyzer(); options.Validator = _umbracoIndexConfig.GetMemberValueSetValidator(); - options.FieldDefinitions = new UmbracoFieldDefinitionCollection(); + options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions); break; case Constants.UmbracoIndexes.DeliveryApiContentIndexName: options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion); @@ -64,6 +70,7 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions public void Configure(LuceneDirectoryIndexOptions options) => throw new NotImplementedException("This is never called and is just part of the interface"); } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs index f2c6236a2e..75a1cfe197 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs @@ -30,11 +30,29 @@ public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection new(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw), }; + /// + /// Initializes a new instance of the class containing + /// the default Umbraco field definitions. + /// public UmbracoFieldDefinitionCollection() : base(UmbracoIndexFieldDefinitions) { } + /// + /// Initializes a new instance of the class containing the containing + /// the default Umbraco field definitions, augmented or overridden by the provided definitions. + /// + /// Existing collection of field definitions. + public UmbracoFieldDefinitionCollection(FieldDefinitionCollection definitions) + : base(UmbracoIndexFieldDefinitions) + { + foreach (FieldDefinition definition in definitions) + { + AddOrUpdate(definition); + } + } + /// /// Overridden to dynamically add field definitions for culture variations /// diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollectionTests.cs new file mode 100644 index 0000000000..98832e2bf1 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollectionTests.cs @@ -0,0 +1,75 @@ +using Examine; +using NUnit.Framework; +using Umbraco.Cms.Infrastructure.Examine; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Examine; + +[TestFixture] +internal class UmbracoFieldDefinitionCollectionTests +{ + [Test] + public void Create_Contains_Expected_Fields() + { + var collection = new UmbracoFieldDefinitionCollection(); + AssertDefaultField(collection); + } + + [Test] + public void Create_New_Contains_Expected_Fields() + { + var collection = new UmbracoFieldDefinitionCollection(); + collection.AddOrUpdate(new FieldDefinition("customField", "string")); + var collectionCount = collection.Count; + + collection = new UmbracoFieldDefinitionCollection(); + Assert.AreEqual(collectionCount - 1, collection.Count); + AssertDefaultField(collection); + AssertCustomField(collection, expectExists: false); + } + + [Test] + public void Create_With_Existing_Contains_Expected_Fields() + { + var collection = new UmbracoFieldDefinitionCollection(); + collection.AddOrUpdate(new FieldDefinition("customField", "string")); + var collectionCount = collection.Count; + + collection = new UmbracoFieldDefinitionCollection(collection); + Assert.AreEqual(collectionCount, collection.Count); + AssertDefaultField(collection); + AssertCustomField(collection, expectExists: true); + } + + [Test] + public void Create_With_Existing_Retains_Override_Of_DefaultField() + { + var collection = new UmbracoFieldDefinitionCollection(); + collection.AddOrUpdate(new FieldDefinition("parentID", "string")); + + collection = new UmbracoFieldDefinitionCollection(collection); + AssertDefaultField(collection, "string"); + } + + private static void AssertDefaultField(UmbracoFieldDefinitionCollection collection, string expectedType = "int") + { + var field = collection.SingleOrDefault(x => x.Name == "parentID"); + Assert.IsNotNull(field); + Assert.AreEqual("parentID", field.Name); + Assert.AreEqual(expectedType, field.Type); + } + + private static void AssertCustomField(UmbracoFieldDefinitionCollection collection, bool expectExists) + { + var field = collection.SingleOrDefault(x => x.Name == "customField"); + if (expectExists is false) + { + Assert.IsNull(field.Name); + Assert.IsNull(field.Type); + return; + } + + Assert.IsNotNull(field); + Assert.AreEqual("customField", field.Name); + Assert.AreEqual("string", field.Type); + } +} From 137aa20a10989081f77bc9e879b8dbc51f1ddfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 24 Nov 2025 15:04:59 +0100 Subject: [PATCH 3/6] Block Editors: avoid discard changes for no changes (Fixes #20680) (#20941) * ensure Block List only updates if it has an update * ensures RTE and Grid Block Editor ony updates value if there is a change --- .../property-editor-ui-block-grid.element.ts | 11 +++++++++-- .../property-editor-ui-block-list.element.ts | 11 +++++++++-- .../rte/components/rte-base.element.ts | 17 ++++++++++++++--- .../BlockGrid/ContentWithBlockGrid.spec.ts | 19 +++++++++++++++++++ .../BlockList/ContentWithBlockList.spec.ts | 18 ++++++++++++++++++ 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts index a36e0f5658..8ab27986e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts @@ -17,7 +17,7 @@ import type { UmbPropertyEditorUiElement, UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; -import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { jsonStringComparison, observeMultiple } from '@umbraco-cms/backoffice/observable-api'; import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import { UmbFormControlMixin, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type'; @@ -181,15 +181,22 @@ export class UmbPropertyEditorUIBlockGridElement ]).pipe(debounceTime(20)), ([layouts, contents, settings, exposes]) => { if (layouts.length === 0) { + if (this.value === undefined) { + return; + } super.value = undefined; } else { - super.value = { + const newValue = { ...super.value, layout: { [UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts }, contentData: contents, settingsData: settings, expose: exposes, }; + if (jsonStringComparison(this.value, newValue)) { + return; + } + super.value = newValue; } // If we don't have a value set from the outside or an internal value, we don't want to set the value. 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 ddc7fe5967..f5169b5062 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 @@ -25,7 +25,7 @@ import { UmbFormControlMixin, UmbValidationContext, } from '@umbraco-cms/backoffice/validation'; -import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { jsonStringComparison, observeMultiple } from '@umbraco-cms/backoffice/observable-api'; import { debounceTime } from '@umbraco-cms/backoffice/external/rxjs'; import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; @@ -339,15 +339,22 @@ export class UmbPropertyEditorUIBlockListElement ]).pipe(debounceTime(20)), ([layouts, contents, settings, exposes]) => { if (layouts.length === 0) { + if (this.value === undefined) { + return; + } super.value = undefined; } else { - super.value = { + const newValue = { ...super.value, layout: { [UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts }, contentData: contents, settingsData: settings, expose: exposes, }; + if (jsonStringComparison(this.value, newValue)) { + return; + } + super.value = newValue; } // If we don't have a value set from the outside or an internal value, we don't want to set the value. diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts index ef841b82fc..e22c9220bd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts @@ -1,6 +1,6 @@ import type { UmbPropertyEditorRteValueType } from '../types.js'; import { UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../constants.js'; -import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { jsonStringComparison, observeMultiple } from '@umbraco-cms/backoffice/observable-api'; import { property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbBlockRteEntriesContext, UmbBlockRteManagerContext } from '@umbraco-cms/backoffice/block-rte'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @@ -243,9 +243,12 @@ export abstract class UmbPropertyEditorUiRteElementBase ([layouts, contents, settings, exposes]) => { if (layouts.length === 0) { if (super.value?.markup === undefined) { + if (this.value === undefined) { + return; + } super.value = undefined; } else { - super.value = { + const newValue = { ...super.value, blocks: { layout: {}, @@ -254,9 +257,13 @@ export abstract class UmbPropertyEditorUiRteElementBase expose: [], }, }; + if (jsonStringComparison(this.value, newValue)) { + return; + } + super.value = newValue; } } else { - super.value = { + const newValue = { markup: this._markup, blocks: { layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts }, @@ -265,6 +272,10 @@ export abstract class UmbPropertyEditorUiRteElementBase expose: exposes, }, }; + if (jsonStringComparison(this.value, newValue)) { + return; + } + super.value = newValue; } // If we don't have a value set from the outside or an internal value, we don't want to set the value. diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts index b16ede4a08..b9d8d9ac96 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts @@ -417,3 +417,22 @@ test('can add a variant block element with invariant RTE Tiptap in the content', await umbracoApi.documentType.ensureNameNotExists(customElementTypeName); await umbracoApi.language.ensureNameNotExists('Danish'); }); + +// Tests regression issue: https://github.com/umbraco/Umbraco-CMS/issues/20680 +test('can move away from a content node with a block grid after making no changes without seeing discard unsaved changes', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeId = await umbracoApi.dataType.createBlockGridWithPermissions(customDataTypeName, elementTypeId, true, true); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Assert + // We do this to make sure that there is no discard changes button visible, if the discard changes was visible, we would not be able to go to the document type + await umbracoUi.documentType.goToDocumentType(documentTypeName); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts index 7fed44f119..6fde9d28b0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts @@ -364,3 +364,21 @@ test('can add a variant block element with invariant RTE Tiptap in the content', await umbracoApi.documentType.ensureNameNotExists(customElementTypeName); await umbracoApi.language.ensureNameNotExists('Danish'); }); + +// Tests regression issue: https://github.com/umbraco/Umbraco-CMS/issues/20680 +test('can move away from a content node with a block list after making no changes without seeing discard unsaved changes', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(customDataTypeName, elementTypeId); + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Assert + // We do this to make sure that there is no discard changes button visible, if the discard changes was visible, we would not be able to go to the document type + await umbracoUi.documentType.goToDocumentType(documentTypeName); +}); From 6e6f822761fea8424c1794efc4fd23919059a873 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:24:58 +0100 Subject: [PATCH 4/6] bumps version to 16.4.0-rc3 --- src/Umbraco.Web.UI.Client/package-lock.json | 4 ++-- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index d0cb95df42..5a2d6f3ec4 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@umbraco-cms/backoffice", - "version": "16.4.0-rc", + "version": "16.4.0-rc3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@umbraco-cms/backoffice", - "version": "16.4.0-rc", + "version": "16.4.0-rc3", "license": "MIT", "workspaces": [ "./src/packages/*", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index b076052b37..4fee7cb51d 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.4.0-rc2", + "version": "16.4.0-rc3", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index cf14b3a81a..027018923c 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "16.4.0-rc2", + "version": "16.4.0-rc3", "assemblyVersion": { "precision": "build" }, From aea9034adf2e64a23ebd7da673584d68caacb107 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:27:35 +0100 Subject: [PATCH 5/6] Localization: Restores region-specific cultures (#20939) (#20942) * Adds localization manifests for region-specific cultures This is to support backwards-compatibility and v13 upgradability. * Removed `uiCulture` from Vietnamese localizations since it duplicated the English fallback texts. * 'en' localization file formatting * Update src/Umbraco.Web.UI.Client/src/assets/lang/en.ts --------- Co-authored-by: Lee Kelleher Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/assets/lang/en.ts | 27 +- .../src/assets/lang/vi.ts | 30 -- .../packages/core/localization/manifests.ts | 278 +++++++++++------- 3 files changed, 191 insertions(+), 144 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 f57a8dbea3..aa6e5766b6 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2869,29 +2869,52 @@ export default { ar: 'العربية', bs: 'Bosanski', cs: 'Česky', + 'cs-cz': 'Česky (Czechia)', cy: 'Cymraeg', + 'cy-gb': 'Cymraeg (UK)', da: 'Dansk', + 'da-dk': 'Dansk (Danmark)', de: 'Deutsch', + 'de-de': 'Deutsch (Deutschland)', + 'de-ch': 'Deutsch (Schweiz)', en: 'English (UK)', 'en-us': 'English (US)', es: 'Español', + 'es-es': 'Español (España)', fr: 'Français', - he: 'Hebrew', + 'fr-fr': 'Français (France)', + 'fr-ch': 'Français (Suisse)', + he: 'עברית', + 'he-il': 'עברית (ישראל)', hr: 'Hrvatski', + 'hr-hr': 'Hrvatski (Hrvatska)', it: 'Italiano', + 'it-it': 'Italiano (Italia)', + 'it-ch': 'Italiano (Svizzera)', ja: '日本語', + 'ja-jp': '日本語 (日本)', ko: '한국어', + 'ko-kr': '한국어 (한국)', nb: 'Norsk Bokmål', + 'nb-no': 'Norsk (Bokmål)', nl: 'Nederlands', + 'nl-nl': 'Nederlands (Nederland)', pl: 'Polski', + 'pl-pl': 'Polski (Polska)', pt: 'Português', 'pt-br': 'Português (Brasil)', - ro: 'Romana', + ro: 'Română', + 'ro-ro': 'Română (România)', ru: 'Русский', + 'ru-ru': 'Русский (Россия)', sv: 'Svenska', + 'sv-se': 'Svenska (Sverige)', tr: 'Türkçe', + 'tr-tr': 'Türkçe (Türkiye Cumhuriyeti)', uk: 'Українська', + 'uk-ua': 'Українська (Україна)', zh: '中文', + 'zh-cn': '中文(简体,中国)', 'zh-tw': '中文(正體,台灣)', vi: 'Tiếng Việt', }, diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/vi.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/vi.ts index 65be353739..ae763698a4 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/vi.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/vi.ts @@ -2836,34 +2836,4 @@ export default { resetUrlMessage: 'Bạn có chắc chắn muốn đặt lại URL này không?', resetUrlLabel: 'Đặt lại', }, - uiCulture: { - ar: 'العربية', - bs: 'Bosanski', - cs: 'Česky', - cy: 'Cymraeg', - da: 'Dansk', - de: 'Deutsch', - en: 'English (UK)', - 'en-us': 'English (US)', - es: 'Español', - fr: 'Français', - he: 'Hebrew', - hr: 'Hrvatski', - it: 'Italiano', - ja: '日本語', - ko: '한국어', - nb: 'Norsk Bokmål', - nl: 'Nederlands', - pl: 'Polski', - pt: 'Português', - 'pt-br': 'Português (Brasil)', - ro: 'Romana', - ru: 'Русский', - sv: 'Svenska', - tr: 'Türkçe', - uk: 'Українська', - zh: '中文', - 'zh-tw': '中文(正體,台灣)', - vi: 'Tiếng Việt', - }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts index 17760f4434..f429831c7c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/manifests.ts @@ -4,281 +4,335 @@ export const manifests: Array = [ { type: 'localization', alias: 'Umb.Localization.AR', - weight: 100, name: 'Arabic Backoffice UI Localization', - meta: { - culture: 'ar', - }, + meta: { culture: 'ar' }, js: () => import('../../../assets/lang/ar.js'), }, { type: 'localization', alias: 'Umb.Localization.BS', - weight: 100, name: 'Bosnian Backoffice UI Localization', - meta: { - culture: 'bs', - }, + meta: { culture: 'bs' }, js: () => import('../../../assets/lang/bs.js'), }, { type: 'localization', alias: 'Umb.Localization.CS', - weight: 100, name: 'Czech Backoffice UI Localization', - meta: { - culture: 'cs', - }, + meta: { culture: 'cs' }, js: () => import('../../../assets/lang/cs.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.CS_CZ', + name: 'Czech (Czechia) Backoffice UI Localization', + meta: { culture: 'cs-CZ' }, + }, { type: 'localization', alias: 'Umb.Localization.CY', - weight: 100, name: 'Welsh Backoffice UI Localization', - meta: { - culture: 'cy', - }, + meta: { culture: 'cy' }, js: () => import('../../../assets/lang/cy.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.CY_GB', + name: 'Welsh (UK) Backoffice UI Localization', + meta: { culture: 'cy-GB' }, + }, { type: 'localization', alias: 'Umb.Localization.DA', - weight: 100, name: 'Danish Backoffice UI Localization', - meta: { - culture: 'da', - }, + meta: { culture: 'da' }, js: () => import('../../../assets/lang/da.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.DA-DK', + name: 'Danish (Denmark) Backoffice UI Localization', + meta: { culture: 'da-DK' }, + }, { type: 'localization', alias: 'Umb.Localization.DE', - weight: 100, name: 'German Backoffice UI Localization', - meta: { - culture: 'de', - }, + meta: { culture: 'de' }, js: () => import('../../../assets/lang/de.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.DE_DE', + name: 'German (Germany) Backoffice UI Localization', + meta: { culture: 'de-DE' }, + }, + { + type: 'localization', + alias: 'Umb.Localization.DE_CH', + name: 'German (Switzerland) Backoffice UI Localization', + meta: { culture: 'de-CH' }, + }, { type: 'localization', alias: 'Umb.Localization.EN', - weight: 100, name: 'English (United Kingdom) Backoffice UI Localization', - meta: { - culture: 'en', - }, + meta: { culture: 'en' }, js: () => import('../../../assets/lang/en.js'), }, { type: 'localization', alias: 'Umb.Localization.EN_US', - weight: 100, name: 'English (United States) Backoffice UI Localization', - meta: { - culture: 'en-US', - }, + meta: { culture: 'en-US' }, js: () => import('../../../assets/lang/en-us.js'), }, { type: 'localization', alias: 'Umb.Localization.ES', - weight: 100, name: 'Spanish Backoffice UI Localization', - meta: { - culture: 'es', - }, + meta: { culture: 'es' }, js: () => import('../../../assets/lang/es.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.ES_ES', + name: 'Spanish (Spain) Backoffice UI Localization', + meta: { culture: 'es-ES' }, + }, { type: 'localization', alias: 'Umb.Localization.FR', - weight: 100, name: 'French Backoffice UI Localization', - meta: { - culture: 'fr', - }, + meta: { culture: 'fr' }, js: () => import('../../../assets/lang/fr.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.FR_FR', + name: 'French (France) Backoffice UI Localization', + meta: { culture: 'fr-FR' }, + }, + { + type: 'localization', + alias: 'Umb.Localization.FR_CH', + name: 'French (Switzerland) Backoffice UI Localization', + meta: { culture: 'fr-CH' }, + }, { type: 'localization', alias: 'Umb.Localization.HE', - weight: 100, name: 'Hebrew Backoffice UI Localization', - meta: { - culture: 'he', - }, + meta: { culture: 'he' }, js: () => import('../../../assets/lang/he.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.HE_IL', + name: 'Hebrew (Israel) Backoffice UI Localization', + meta: { culture: 'he-IL' }, + }, { type: 'localization', alias: 'Umb.Localization.HR', - weight: 100, name: 'Croatian Backoffice UI Localization', - meta: { - culture: 'hr', - }, + meta: { culture: 'hr' }, js: () => import('../../../assets/lang/hr.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.HR_HR', + name: 'Croatian (Croatia) Backoffice UI Localization', + meta: { culture: 'hr-HR' }, + }, { type: 'localization', alias: 'Umb.Localization.IT', - weight: 100, name: 'Italian Backoffice UI Localization', - meta: { - culture: 'it', - }, + meta: { culture: 'it' }, js: () => import('../../../assets/lang/it.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.IT_IT', + name: 'Italian (Italy) Backoffice UI Localization', + meta: { culture: 'it-IT' }, + }, + { + type: 'localization', + alias: 'Umb.Localization.IT_CH', + name: 'Italian (Switzerland) Backoffice UI Localization', + meta: { culture: 'it-CH' }, + }, { type: 'localization', alias: 'Umb.Localization.JA', - weight: 100, name: 'Japanese Backoffice UI Localization', - meta: { - culture: 'ja', - }, + meta: { culture: 'ja' }, js: () => import('../../../assets/lang/ja.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.JA_JP', + name: 'Japanese (Japan) Backoffice UI Localization', + meta: { culture: 'ja-JP' }, + }, { type: 'localization', alias: 'Umb.Localization.KO', - weight: 100, name: 'Korean Backoffice UI Localization', - meta: { - culture: 'ko', - }, + meta: { culture: 'ko' }, js: () => import('../../../assets/lang/ko.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.KO_KR', + name: 'Korean (Korea) Backoffice UI Localization', + meta: { culture: 'ko-KR' }, + }, { type: 'localization', alias: 'Umb.Localization.NB', - weight: 100, name: 'Norwegian Backoffice UI Localization', - meta: { - culture: 'nb', - }, + meta: { culture: 'nb' }, js: () => import('../../../assets/lang/nb.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.NB_NO', + name: 'Norwegian (Norway) Backoffice UI Localization', + meta: { culture: 'nb-NO' }, + }, { type: 'localization', alias: 'Umb.Localization.NL', - weight: 100, name: 'Dutch Backoffice UI Localization', - meta: { - culture: 'nl', - }, + meta: { culture: 'nl' }, js: () => import('../../../assets/lang/nl.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.NL_NL', + name: 'Dutch (Netherlands) Backoffice UI Localization', + meta: { culture: 'nl-NL' }, + }, { type: 'localization', alias: 'Umb.Localization.PL', - weight: 100, name: 'Polish Backoffice UI Localization', - meta: { - culture: 'pl', - }, + meta: { culture: 'pl' }, js: () => import('../../../assets/lang/pl.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.PL_PL', + name: 'Polish (Poland) Backoffice UI Localization', + meta: { culture: 'pl-PL' }, + }, { type: 'localization', alias: 'Umb.Localization.PT', - weight: 100, name: 'Portuguese Backoffice UI Localization', - meta: { - culture: 'pt', - }, + meta: { culture: 'pt' }, js: () => import('../../../assets/lang/pt.js'), }, { type: 'localization', alias: 'Umb.Localization.PT_BR', - weight: 100, name: 'Portuguese (Brazil) Backoffice UI Localization', - meta: { - culture: 'pt-BR', - }, + meta: { culture: 'pt-BR' }, js: () => import('../../../assets/lang/pt-br.js'), }, { type: 'localization', alias: 'Umb.Localization.RO', - weight: 100, name: 'Romanian Backoffice UI Localization', - meta: { - culture: 'ro', - }, + meta: { culture: 'ro' }, js: () => import('../../../assets/lang/ro.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.RO_RO', + name: 'Romanian (Romania) Backoffice UI Localization', + meta: { culture: 'ro-RO' }, + }, { type: 'localization', alias: 'Umb.Localization.RU', - weight: 100, name: 'Russian Backoffice UI Localization', - meta: { - culture: 'ru', - }, + meta: { culture: 'ru' }, js: () => import('../../../assets/lang/ru.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.RU_RU', + name: 'Russian (Russia) Backoffice UI Localization', + meta: { culture: 'ru-RU' }, + }, { type: 'localization', alias: 'Umb.Localization.SV', - weight: 100, name: 'Swedish Backoffice UI Localization', - meta: { - culture: 'sv', - }, + meta: { culture: 'sv' }, js: () => import('../../../assets/lang/sv.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.SV_SE', + name: 'Swedish (Sweden) Backoffice UI Localization', + meta: { culture: 'sv-SE' }, + }, { type: 'localization', alias: 'Umb.Localization.TR', - weight: 100, name: 'Turkish Backoffice UI Localization', - meta: { - culture: 'tr', - }, + meta: { culture: 'tr' }, js: () => import('../../../assets/lang/tr.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.TR_TR', + name: 'Turkish (Türkiye) Backoffice UI Localization', + meta: { culture: 'tr-TR' }, + }, { type: 'localization', alias: 'Umb.Localization.UK', - weight: 100, name: 'Ukrainian Backoffice UI Localization', - meta: { - culture: 'uk', - }, + meta: { culture: 'uk' }, js: () => import('../../../assets/lang/uk.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.UK_UA', + name: 'Ukrainian (Ukraine) Backoffice UI Localization', + meta: { culture: 'uk-UA' }, + }, { type: 'localization', alias: 'Umb.Localization.ZH', - weight: 100, name: 'Chinese Backoffice UI Localization', - meta: { - culture: 'zh', - }, + meta: { culture: 'zh' }, js: () => import('../../../assets/lang/zh.js'), }, + { + type: 'localization', + alias: 'Umb.Localization.ZH_CN', + name: 'Chinese (Simplified, China) Backoffice UI Localization', + meta: { culture: 'zh-CN' }, + }, { type: 'localization', alias: 'Umb.Localization.ZH_TW', - weight: 100, name: 'Chinese (Taiwan) Backoffice UI Localization', - meta: { - culture: 'zh-TW', - }, + meta: { culture: 'zh-TW' }, js: () => import('../../../assets/lang/zh-tw.js'), }, { type: 'localization', alias: 'Umb.Localization.VI', - weight: 100, name: 'Vietnamese Backoffice UI Localization', - meta: { - culture: 'vi', - }, + meta: { culture: 'vi' }, js: () => import('../../../assets/lang/vi.js'), }, ]; From 05431638176a5caae006a1fbbb1ab31300163fd7 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:29:11 +0100 Subject: [PATCH 6/6] bumps version to 16.5.0-rc --- src/Umbraco.Web.UI.Client/package-lock.json | 4 ++-- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 5a2d6f3ec4..b914a47247 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@umbraco-cms/backoffice", - "version": "16.4.0-rc3", + "version": "16.5.0-rc", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@umbraco-cms/backoffice", - "version": "16.4.0-rc3", + "version": "16.5.0-rc", "license": "MIT", "workspaces": [ "./src/packages/*", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 4fee7cb51d..98e110f515 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.4.0-rc3", + "version": "16.5.0-rc", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index 027018923c..b58b485563 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "16.4.0-rc3", + "version": "16.5.0-rc", "assemblyVersion": { "precision": "build" },