From 046a3d9aadd520e43f93d0bf9b1e711b79c44384 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Wed, 6 Aug 2025 14:51:21 +0100 Subject: [PATCH] Tiptap RTE: Undo deleted blocks (#19851) * RTE: Restore deleted blocks Maintains a state of unused (deleted) blocks, that could be restored later, e.g. with Tiptap RTE's undo action. Fixes #19637 * Updated with @copilot suggestions * Fixes restored block state on variant documents --- .../rte/components/rte-base.element.ts | 80 ++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) 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 602ea5fcf7..ea01c96fd8 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 @@ -10,14 +10,16 @@ import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; -import type { UmbBlockRteTypeModel } from '@umbraco-cms/backoffice/block-rte'; +import type { StyleInfo } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbBlockDataModel } from '@umbraco-cms/backoffice/block'; +import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '@umbraco-cms/backoffice/block-rte'; import type { UmbPropertyEditorUiElement, UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; -import type { StyleInfo } from '@umbraco-cms/backoffice/external/lit'; export abstract class UmbPropertyEditorUiRteElementBase extends UmbFormControlMixin(UmbLitElement) @@ -127,6 +129,10 @@ export abstract class UmbPropertyEditorUiRteElementBase readonly #validationContext = new UmbValidationContext(this); + readonly #unusedLayoutLookup: Map = new Map(); + readonly #unusedContentLookup: Map = new Map(); + readonly #unusedSettingsLookup: Map = new Map(); + constructor() { super(); @@ -262,9 +268,72 @@ export abstract class UmbPropertyEditorUiRteElementBase ); } + #setUnusedBlockLookups(unusedLayouts: Array) { + if (unusedLayouts.length) { + unusedLayouts.forEach((layout) => { + if (layout.contentKey) { + this.#unusedLayoutLookup.set(layout.contentKey, layout); + + const contentBlock = this.#managerContext.getContentOf(layout.contentKey); + if (contentBlock) { + this.#unusedContentLookup.set(layout.contentKey, contentBlock); + } else { + console.warn( + `Expected content block for '${layout.contentKey}' was not found. This may indicate a data consistency issue.`, + ); + } + + if (layout.settingsKey) { + const settingsBlock = this.#managerContext.getSettingsOf(layout.settingsKey); + if (settingsBlock) { + this.#unusedSettingsLookup.set(layout.settingsKey, settingsBlock); + } else { + console.warn( + `Expected settings block for '${layout.settingsKey}' was not found. This may indicate a data consistency issue.`, + ); + } + } + } + }); + } + } + + #restoreUnusedBlocks(usedContentKeys: Array) { + if (usedContentKeys.length) { + usedContentKeys.forEach((contentKey) => { + if (contentKey && this.#unusedLayoutLookup.has(contentKey)) { + const layout = this.#unusedLayoutLookup.get(contentKey); + if (layout) { + this.#managerContext.setOneLayout(layout); + this.#unusedLayoutLookup.delete(contentKey); + + const contentBlock = this.#unusedContentLookup.get(contentKey); + if (contentBlock) { + this.#managerContext.setOneContent(contentBlock); + this.#managerContext.setOneExpose(contentKey, UmbVariantId.CreateInvariant()); + this.#unusedContentLookup.delete(contentKey); + } + + if (layout.settingsKey && this.#unusedSettingsLookup.has(layout.settingsKey)) { + const settingsBlock = this.#unusedSettingsLookup.get(layout.settingsKey); + if (settingsBlock) { + this.#managerContext.setOneSettings(settingsBlock); + this.#unusedSettingsLookup.delete(layout.settingsKey); + } + } + } + } + }); + } + } + protected _filterUnusedBlocks(usedContentKeys: (string | null)[]) { const unusedLayouts = this.#managerContext.getLayouts().filter((x) => usedContentKeys.indexOf(x.contentKey) === -1); + // Temporarily set the unused layouts to the lookup, as they could be restored later, e.g. via an RTE undo action. [LK] + this.#restoreUnusedBlocks(usedContentKeys); + this.#setUnusedBlockLookups(unusedLayouts); + const unusedContentKeys = unusedLayouts.map((x) => x.contentKey); const unusedSettingsKeys = unusedLayouts @@ -279,4 +348,11 @@ export abstract class UmbPropertyEditorUiRteElementBase protected _fireChangeEvent() { this.dispatchEvent(new UmbChangeEvent()); } + + override destroy() { + super.destroy(); + this.#unusedLayoutLookup.clear(); + this.#unusedContentLookup.clear(); + this.#unusedSettingsLookup.clear(); + } }