From c20b30d625e1b23f3f49fae7e06e2826657408c2 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:10:02 +0200 Subject: [PATCH 01/10] Fix null reference exception when removing all blocks from shared RTE in culture variant content (#19771) Fix null reference exception when removing all blocks from culture variant content --- .../PropertyEditors/BlockValuePropertyValueEditorBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index 52c02c46bc..18578d495f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -362,7 +362,7 @@ public abstract class BlockValuePropertyValueEditorBase : DataV var mergedInvariant = UpdateSourceInvariantData(source, target, canUpdateInvariantData); // if the structure (invariant) is not defined after merger, the target content does not matter - if (mergedInvariant is null) + if (mergedInvariant?.Layout is null) { return null; } @@ -393,14 +393,14 @@ public abstract class BlockValuePropertyValueEditorBase : DataV RestoreMissingValues( source.BlockValue.ContentData, target.BlockValue.ContentData, - mergedInvariant.Layout!, + mergedInvariant.Layout, (layoutItem, itemData) => layoutItem.ContentKey == itemData.Key, canUpdateInvariantData, allowedCultures); RestoreMissingValues( source.BlockValue.SettingsData, target.BlockValue.SettingsData, - mergedInvariant.Layout!, + mergedInvariant.Layout, (layoutItem, itemData) => layoutItem.SettingsKey == itemData.Key, canUpdateInvariantData, allowedCultures); From a5612107a6a457faf9ab985588e7846744d094e7 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 24 Jul 2025 06:48:27 +0200 Subject: [PATCH 02/10] Bumped version to 16.1.0. --- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 3de46d0ca5..1972dafada 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.1.0-rc", + "version": "16.1.0", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index a511502dde..311ba25a6a 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.1.0-rc", + "version": "16.1.0", "assemblyVersion": { "precision": "build" }, From 12141d2d61dc571e79fa491c2f5ef7ce28bfb795 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Thu, 24 Jul 2025 06:07:29 +0100 Subject: [PATCH 03/10] Tiptap RTE: Clear Formatting, resets nodes to "paragraph" (#19781) Fixes #19752 --- .../extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts index 9da1b318f4..4f0342731f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/clear-formatting.tiptap-toolbar-api.ts @@ -3,6 +3,6 @@ import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapToolbarClearFormattingExtensionApi extends UmbTiptapToolbarElementApiBase { override execute(editor?: Editor) { - editor?.chain().focus().unsetAllMarks().unsetClassName().unsetStyles().run(); + editor?.chain().focus().clearNodes().unsetAllMarks().unsetClassName().unsetStyles().run(); } } From 0b4208535357e720cfa77267e067c23b5cb0a1eb Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 24 Jul 2025 09:25:52 +0200 Subject: [PATCH 04/10] Bumped version to 16.1.1. --- src/Umbraco.Web.UI.Client/package.json | 2 +- version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 1972dafada..6459f7d0a4 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.1.0", + "version": "16.1.1", "type": "module", "exports": { ".": null, diff --git a/version.json b/version.json index 311ba25a6a..ba53bc4b4b 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.1.0", + "version": "16.1.1", "assemblyVersion": { "precision": "build" }, From a2cc6a0a87f1500e8827b6afc49abd5c262f0d62 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Fri, 25 Jul 2025 13:07:20 +0200 Subject: [PATCH 05/10] Fix issue with use of EF Core scopes within notification handlers (take 2 - handling scopes with a base parent) (#19797) * Add integration tests that shows the problem * Fix the problem and add explenation * Improved comments slightly to help when we come back here! Moved tests alongside existing ones related to scopes. Removed long running attribute from tests (they are quite fast). * Fixed casing in comment. --------- Co-authored-by: Andy Butland --- .../Scoping/EFCoreScope.cs | 8 +- src/Umbraco.Core/Scoping/CoreScope.cs | 2 + .../Scoping/NestedScopeTests.cs | 222 ++++++++++++++++++ 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/NestedScopeTests.cs diff --git a/src/Umbraco.Cms.Persistence.EFCore/Scoping/EFCoreScope.cs b/src/Umbraco.Cms.Persistence.EFCore/Scoping/EFCoreScope.cs index 461b09334c..382bcf5593 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Scoping/EFCoreScope.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Scoping/EFCoreScope.cs @@ -127,10 +127,16 @@ internal class EFCoreScope : CoreScope, IEfCoreScope Locks.ClearLocks(InstanceId); - if (ParentScope is null) + // Since we can nest EFCoreScopes in other scopes derived from CoreScope, we should check whether our ParentScope OR the base ParentScope exists. + // Only if neither do do we take responsibility for ensuring the locks are cleared. + // Eventually the highest parent will clear the locks. + // Further, these locks are a reference to the locks of the highest parent anyway (see the constructor of CoreScope). +#pragma warning disable SA1100 // Do not prefix calls with base unless local implementation exists (justification: provides additional clarify here that this is defined on the base class). + if (ParentScope is null && base.HasParentScope is false) { Locks.EnsureLocksCleared(InstanceId); } +#pragma warning restore SA1100 // Do not prefix calls with base unless local implementation exists _efCoreScopeProvider.PopAmbientScope(); diff --git a/src/Umbraco.Core/Scoping/CoreScope.cs b/src/Umbraco.Core/Scoping/CoreScope.cs index 7fe6c400fb..e158641af4 100644 --- a/src/Umbraco.Core/Scoping/CoreScope.cs +++ b/src/Umbraco.Core/Scoping/CoreScope.cs @@ -250,6 +250,8 @@ public class CoreScope : ICoreScope _parentScope = coreScope; } + protected bool HasParentScope => _parentScope is not null; + protected void HandleScopedNotifications() => _notificationPublisher?.ScopeExit(Completed.HasValue && Completed.Value); private void EnsureNotDisposed() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/NestedScopeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/NestedScopeTests.cs new file mode 100644 index 0000000000..4d0587c236 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/NestedScopeTests.cs @@ -0,0 +1,222 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Persistence.EFCore.Scoping; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Cms.Tests.Integration.Umbraco.Persistence.EFCore.DbContext; +using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping +{ + /// + /// These tests verify that the various types of scopes we have can be created and disposed within each other. + /// + /// + /// Scopes are: + /// - "Normal" - created by "/>. + /// - "Core" - created by "/>. + /// - "EFCore" - created by "/>. + /// + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + internal sealed class NestedScopeTests : UmbracoIntegrationTest + { + private new IScopeProvider ScopeProvider => Services.GetRequiredService(); + + private ICoreScopeProvider CoreScopeProvider => Services.GetRequiredService(); + + private IEFCoreScopeProvider EfCoreScopeProvider => + Services.GetRequiredService>(); + + [Test] + public void CanNestScopes_Normal_Core_EfCore() + { + using (var ambientScope = ScopeProvider.CreateScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var outerScope = CoreScopeProvider.CreateCoreScope()) + { + outerScope.WriteLock(Constants.Locks.ContentTree); + + using (var innerScope = EfCoreScopeProvider.CreateScope()) + { + innerScope.WriteLock(Constants.Locks.ContentTree); + + innerScope.Complete(); + outerScope.Complete(); + ambientScope.Complete(); + } + } + } + } + + [Test] + public void CanNestScopes_Normal_EfCore_Core() + { + using (var ambientScope = ScopeProvider.CreateScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var outerScope = EfCoreScopeProvider.CreateScope()) + { + outerScope.WriteLock(Constants.Locks.ContentTree); + + using (var innerScope = CoreScopeProvider.CreateCoreScope()) + { + innerScope.WriteLock(Constants.Locks.ContentTree); + + innerScope.Complete(); + outerScope.Complete(); + ambientScope.Complete(); + } + } + } + } + + [Test] + public void CanNestScopes_Core_Normal_Efcore() + { + using (var ambientScope = CoreScopeProvider.CreateCoreScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var outerScope = ScopeProvider.CreateScope()) + { + outerScope.WriteLock(Constants.Locks.ContentTree); + + using (var innerScope = EfCoreScopeProvider.CreateScope()) + { + innerScope.WriteLock(Constants.Locks.ContentTree); + + innerScope.Complete(); + outerScope.Complete(); + ambientScope.Complete(); + } + } + } + } + + [Test] + public void CanNestScopes_Core_EfCore_Normal() + { + using (var ambientScope = CoreScopeProvider.CreateCoreScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var outerScope = EfCoreScopeProvider.CreateScope()) + { + outerScope.WriteLock(Constants.Locks.ContentTree); + + using (var innerScope = ScopeProvider.CreateScope()) + { + innerScope.WriteLock(Constants.Locks.ContentTree); + + innerScope.Complete(); + outerScope.Complete(); + ambientScope.Complete(); + } + } + } + } + + [Test] + public void CanNestScopes_EfCore_Normal_Core() + { + using (var ambientScope = EfCoreScopeProvider.CreateScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var outerScope = ScopeProvider.CreateScope()) + { + outerScope.WriteLock(Constants.Locks.ContentTree); + + using (var innerScope = CoreScopeProvider.CreateCoreScope()) + { + innerScope.WriteLock(Constants.Locks.ContentTree); + + innerScope.Complete(); + outerScope.Complete(); + ambientScope.Complete(); + } + } + } + } + + [Test] + public void CanNestScopes_EfCore_Core_Normal() + { + using (var ambientScope = EfCoreScopeProvider.CreateScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var outerScope = CoreScopeProvider.CreateCoreScope()) + { + outerScope.WriteLock(Constants.Locks.ContentTree); + + using (var innerScope = ScopeProvider.CreateScope()) + { + innerScope.WriteLock(Constants.Locks.ContentTree); + + innerScope.Complete(); + outerScope.Complete(); + ambientScope.Complete(); + } + } + } + } + + [Test] + public void CanNestScopes_Normal_Normal() + { + using (var ambientScope = ScopeProvider.CreateScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var inner = ScopeProvider.CreateScope()) + { + inner.WriteLock(Constants.Locks.ContentTree); + + inner.Complete(); + ambientScope.Complete(); + } + } + } + + [Test] + public void CanNestScopes_Core_Core() + { + using (var ambientScope = CoreScopeProvider.CreateCoreScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var inner = CoreScopeProvider.CreateCoreScope()) + { + inner.WriteLock(Constants.Locks.ContentTree); + + inner.Complete(); + ambientScope.Complete(); + } + } + } + + [Test] + public void CanNestScopes_EfCore_EfCore() + { + using (var ambientScope = EfCoreScopeProvider.CreateScope()) + { + ambientScope.WriteLock(Constants.Locks.ContentTree); + + using (var inner = EfCoreScopeProvider.CreateScope()) + { + inner.WriteLock(Constants.Locks.ContentTree); + + inner.Complete(); + ambientScope.Complete(); + } + } + } + } +} From 7e82c258eebaa595eadc9b000461e27d02bc030e Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 29 Jul 2025 05:10:52 +0200 Subject: [PATCH 06/10] Merge commit from fork Co-authored-by: kjac --- .../Caching/DeliveryApiOutputCachePolicy.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs b/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs index da1580554c..0f318c9602 100644 --- a/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs +++ b/src/Umbraco.Cms.Api.Delivery/Caching/DeliveryApiOutputCachePolicy.cs @@ -18,7 +18,12 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy .RequestServices .GetRequiredService(); - context.EnableOutputCaching = requestPreviewService.IsPreview() is false; + IApiAccessService apiAccessService = context + .HttpContext + .RequestServices + .GetRequiredService(); + + context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess(); context.ResponseExpirationTimeSpan = _duration; return ValueTask.CompletedTask; From 75c7d00b535d9e064ec7d7f047f6bcb631aa64cb Mon Sep 17 00:00:00 2001 From: NguyenThuyLan <116753400+NguyenThuyLan@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:21:17 +0700 Subject: [PATCH 07/10] Fix issue unsaved changes always displayed when trying to move away from blueprint (#19804) Co-authored-by: Lan Nguyen Thuy --- .../src/packages/documents/document-blueprints/entity.ts | 3 +++ .../document-blueprint-detail.server.data-source.ts | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity.ts index 73297b923f..191d091a95 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity.ts @@ -6,6 +6,9 @@ export type UmbDocumentBlueprintRootEntityType = typeof UMB_DOCUMENT_BLUEPRINT_R export type UmbDocumentBlueprintEntityType = typeof UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE; export type UmbDocumentBlueprintFolderEntityType = typeof UMB_DOCUMENT_BLUEPRINT_FOLDER_ENTITY_TYPE; +export const UMB_DOCUMENT_BLUEPRINT_PROPERTY_VALUE_ENTITY_TYPE = `${UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE}-property-value`; +export type UmbDocumentBlueprintPropertyValueEntityType = typeof UMB_DOCUMENT_BLUEPRINT_PROPERTY_VALUE_ENTITY_TYPE; + export type UmbDocumentBlueprintEntityTypeUnion = | UmbDocumentBlueprintRootEntityType | UmbDocumentBlueprintEntityType diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts index 77a9c024a2..b0170e5517 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts @@ -1,5 +1,5 @@ import type { UmbDocumentBlueprintDetailModel } from '../../types.js'; -import { UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE } from '../../entity.js'; +import { UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE, UMB_DOCUMENT_BLUEPRINT_PROPERTY_VALUE_ENTITY_TYPE } from '../../entity.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbDataSourceResponse, UmbDetailDataSource } from '@umbraco-cms/backoffice/repository'; import type { @@ -9,7 +9,6 @@ import type { } from '@umbraco-cms/backoffice/external/backend-api'; import { DocumentBlueprintService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE } from '@umbraco-cms/backoffice/document'; import { tryExecute } from '@umbraco-cms/backoffice/resources'; /** @@ -194,7 +193,7 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource values: data.values.map((value) => { return { editorAlias: value.editorAlias, - entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, + entityType: UMB_DOCUMENT_BLUEPRINT_PROPERTY_VALUE_ENTITY_TYPE, culture: value.culture || null, segment: value.segment || null, alias: value.alias, @@ -203,13 +202,15 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource }), variants: data.variants.map((variant) => { return { - state: variant.state, culture: variant.culture || null, segment: variant.segment || null, + state: variant.state, name: variant.name, publishDate: variant.publishDate || null, createDate: variant.createDate, updateDate: variant.updateDate, + scheduledPublishDate: variant.scheduledPublishDate || null, + scheduledUnpublishDate: variant.scheduledUnpublishDate || null }; }), documentType: { From fb9a9b38a8b5c95b85328dc97fa4b91a86101e42 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Tue, 29 Jul 2025 06:58:41 +0100 Subject: [PATCH 08/10] Tiptap RTE: Include Tiptap's default styles (#19805) * Disables Tiptap's `injectCSS` option This option would inject the default CSS styles into the `window.document`, which are never applied to the component's shadow DOM. * Add Tiptap's default styles to "rte-content.css" The `caret-color` rule (line 93) resolves issue #19791. --- .../src/css/rte-content.css | 79 +++++++++++++++++++ .../input-tiptap/input-tiptap.element.ts | 1 + 2 files changed, 80 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/css/rte-content.css b/src/Umbraco.Web.UI.Client/src/css/rte-content.css index 681920b919..265d736932 100644 --- a/src/Umbraco.Web.UI.Client/src/css/rte-content.css +++ b/src/Umbraco.Web.UI.Client/src/css/rte-content.css @@ -19,3 +19,82 @@ padding: 10px; border-radius: 5px; } + +/* Default Tiptap RTE styles. + * Copied from: https://github.com/ueberdosis/tiptap/blob/v2.11.7/packages/core/src/style.ts + * as we disable the `injectCSS` option in the Tiptap editor. + */ + +.ProseMirror { + position: relative; +} + +.ProseMirror { + word-wrap: break-word; + white-space: pre-wrap; + white-space: break-spaces; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; + font-feature-settings: 'liga' 0; /* the above doesn't seem to work in Edge */ +} + +.ProseMirror [contenteditable='false'] { + white-space: normal; +} + +.ProseMirror [contenteditable='false'] [contenteditable='true'] { + white-space: pre-wrap; +} + +.ProseMirror pre { + white-space: pre-wrap; +} + +img.ProseMirror-separator { + display: inline !important; + border: none !important; + margin: 0 !important; + width: 0 !important; + height: 0 !important; +} + +.ProseMirror-gapcursor { + display: none; + pointer-events: none; + position: absolute; + margin: 0; +} + +.ProseMirror-gapcursor:after { + content: ''; + display: block; + position: absolute; + top: -2px; + width: 20px; + border-top: 1px solid black; + animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; +} + +@keyframes ProseMirror-cursor-blink { + to { + visibility: hidden; + } +} + +.ProseMirror-hideselection *::selection { + background: transparent; +} + +.ProseMirror-hideselection *::-moz-selection { + background: transparent; +} + +.ProseMirror-hideselection * { + caret-color: transparent; +} + +.ProseMirror-focused .ProseMirror-gapcursor { + display: block; +} + +/* End of default Tiptap RTE styles */ diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index f636cf0a96..0fbe9f5480 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -175,6 +175,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin { this._extensions.forEach((ext) => ext.setEditor(editor)); From 34989307db9ad5a6feef34476d8520eec7470c4b Mon Sep 17 00:00:00 2001 From: Lucas Bach Bisgaard Date: Tue, 29 Jul 2025 19:01:22 +0200 Subject: [PATCH 09/10] Change hardcoded text to be translatedeable (#19745) * Change hardcoded text to be translatedeable * Added the `count` value to the localization --------- Co-authored-by: Lucas Bach Bisgaard Co-authored-by: leekelleher --- src/Umbraco.Web.UI.Client/src/assets/lang/da.ts | 2 ++ src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 2 ++ .../statusbar/word-count.tiptap-statusbar-element.ts | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) 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 ed0569656a..3c08a9a05a 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts @@ -2823,6 +2823,8 @@ export default { charmap_extlatin: 'Udvidet latinsk', charmap_symbols: 'Symboler', charmap_arrows: 'Pile', + statusbar_characters: (count: number) => `${count.toLocaleString()} tegn`, + statusbar_words: (count: number) => `${count.toLocaleString()} ord`, }, collection: { noItemsTitle: 'Intet indhold', 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 f538f741d5..93e967fb81 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2812,6 +2812,8 @@ export default { charmap_extlatin: 'Extended Latin', charmap_symbols: 'Symbols', charmap_arrows: 'Arrows', + statusbar_characters: (count: number) => `${count.toLocaleString()} ${count === 1 ? 'character' : 'characters'}`, + statusbar_words: (count: number) => `${count.toLocaleString()} ${count === 1 ? 'word' : 'words'}`, }, linkPicker: { modalSource: 'Source', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/word-count.tiptap-statusbar-element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/word-count.tiptap-statusbar-element.ts index f18a268ce3..e3e67dafa1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/word-count.tiptap-statusbar-element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/word-count.tiptap-statusbar-element.ts @@ -41,8 +41,8 @@ export class UmbTiptapStatusbarWordCountElement extends UmbLitElement { override render() { const label = this._showCharacters - ? this._characters.toLocaleString() + ' ' + (this._characters === 1 ? 'character' : 'characters') - : this._words.toLocaleString() + ' ' + (this._words === 1 ? 'word' : 'words'); + ? this.localize.term('tiptap_statusbar_characters', this._characters) + : this.localize.term('tiptap_statusbar_words', this._words); return html``; } } From 133796f2dd43376587e1355096c465acbabfb09a Mon Sep 17 00:00:00 2001 From: hifi-phil Date: Wed, 30 Jul 2025 06:44:23 +0100 Subject: [PATCH 10/10] V16/docs work extensions example (#19809) * update workspace example * Update readme for workspace counter example * update workspace counter examples readme --- .../workspace-context-counter/README.md | 21 +++++++--- .../counter-status-footer-app.element.ts | 38 +++++++++++++++++++ .../counter-workspace-context.ts | 4 ++ .../workspace-context-counter/index.ts | 26 +++++++++++++ .../reset-counter-menu-item.action.ts | 20 ++++++++++ 5 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/examples/workspace-context-counter/counter-status-footer-app.element.ts create mode 100644 src/Umbraco.Web.UI.Client/examples/workspace-context-counter/reset-counter-menu-item.action.ts diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/README.md b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/README.md index 04336ea3dc..8ccc705531 100644 --- a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/README.md +++ b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/README.md @@ -1,8 +1,19 @@ -# Workspace Context Counter Example +# Workspace Extensions Complete Example -This example demonstrates the essence of the Workspace Context. +The Workspace Context serves as the central communication hub for all workspace extensions. In this example, the context manages a counter that can be manipulated and displayed by different extension types, showcasing the power of shared state management in workspace extensions. -The Workspace Context is available for everything within the Workspace, giving any extension within the ability to communicate through this. -In this example, the Workspace Context houses a counter, which can be incremented by a Workspace Action and shown in the Workspace View. +## Extension types included -To demonstrate this, the example comes with: A Workspace Context, A Workspace Action and a Workspace View. +This complete example includes: + +- **Workspace Context** - Manages shared counter state and provides communication between extensions +- **Workspace Action** - Primary "Increment" button that increases the counter value +- **Workspace Action Menu Item** - "Reset Counter" dropdown option that resets the counter to zero +- **Workspace View** - Dedicated tab that displays the current counter value +- **Workspace Footer App** - Status indicator showing the counter value in the workspace footer + +## How it works + +All extensions communicate through the shared workspace context. When you increment or reset the counter using the actions, the workspace view and footer app automatically update to show the new value, demonstrating reactive state management across workspace extensions. + +This pattern shows how workspace extensions can work together to create cohesive functionality within Umbraco workspaces. \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/counter-status-footer-app.element.ts b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/counter-status-footer-app.element.ts new file mode 100644 index 0000000000..5910e5c7a2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/counter-status-footer-app.element.ts @@ -0,0 +1,38 @@ +import { customElement, html, state, LitElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; +import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context.js'; + +@customElement('example-counter-status-footer-app') +export class ExampleCounterStatusFooterAppElement extends UmbElementMixin(LitElement) { + @state() + private _counter = 0; + + constructor() { + super(); + this.#observeCounter(); + } + + async #observeCounter() { + const context = await this.getContext(EXAMPLE_COUNTER_CONTEXT); + if (!context) return; + + this.observe( + context.counter, + (counter: number) => { + this._counter = counter; + }, + ); + } + + override render() { + return html`Counter: ${this._counter}`; + } +} + +export default ExampleCounterStatusFooterAppElement; + +declare global { + interface HTMLElementTagNameMap { + 'example-counter-status-footer-app': ExampleCounterStatusFooterAppElement; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/counter-workspace-context.ts b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/counter-workspace-context.ts index 5f3d813f54..203a6f1328 100644 --- a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/counter-workspace-context.ts +++ b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/counter-workspace-context.ts @@ -17,6 +17,10 @@ export class WorkspaceContextCounterElement extends UmbContextBase { increment() { this.#counter.setValue(this.#counter.value + 1); } + + reset() { + this.#counter.setValue(0); + } } // Declare a api export, so Extension Registry can initialize this class: diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/index.ts b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/index.ts index edc248b4a9..0f169bc244 100644 --- a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/index.ts +++ b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/index.ts @@ -50,4 +50,30 @@ export const manifests: Array = [ }, ], }, + { + type: 'workspaceActionMenuItem', + kind: 'default', + alias: 'example.workspaceActionMenuItem.resetCounter', + name: 'Reset Counter Menu Item', + api: () => import('./reset-counter-menu-item.action.js'), + forWorkspaceActions: 'example.workspaceAction.incrementor', + weight: 100, + meta: { + label: 'Reset Counter', + icon: 'icon-refresh', + }, + }, + { + type: 'workspaceFooterApp', + alias: 'example.workspaceFooterApp.counterStatus', + name: 'Counter Status Footer App', + element: () => import('./counter-status-footer-app.element.js'), + weight: 900, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], + }, ]; diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/reset-counter-menu-item.action.ts b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/reset-counter-menu-item.action.ts new file mode 100644 index 0000000000..e6db48c8bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/reset-counter-menu-item.action.ts @@ -0,0 +1,20 @@ +import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context.js'; +import { UmbWorkspaceActionMenuItemBase } from '@umbraco-cms/backoffice/workspace'; +import type { UmbWorkspaceActionMenuItem } from '@umbraco-cms/backoffice/workspace'; + +export class ExampleResetCounterMenuItemAction extends UmbWorkspaceActionMenuItemBase implements UmbWorkspaceActionMenuItem { + /** + * This method is executed when the menu item is clicked + */ + override async execute() { + const context = await this.getContext(EXAMPLE_COUNTER_CONTEXT); + if (!context) { + throw new Error('Could not get the counter context'); + } + + // Reset the counter to 0 + context.reset(); + } +} + +export const api = ExampleResetCounterMenuItemAction; \ No newline at end of file