From 6effe321366e7c07b9d32a11e7f8b9a671e9be62 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 21 Oct 2025 20:46:16 +0200 Subject: [PATCH 1/2] Document Recycle Bin: Fix missing item name when restoring (#20599) allow to pass in a item data resolver --- .../restore-from-recycle-bin-modal.element.ts | 42 +++++++++++++------ .../restore-from-recycle-bin-modal.token.ts | 2 + .../restore-from-recycle-bin.action.ts | 1 + .../restore-from-recycle-bin/types.ts | 2 + .../recycle-bin/entity-action/manifests.ts | 1 + 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.element.ts index 8370cc78b0..e8454907f6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.element.ts @@ -10,9 +10,7 @@ import { UmbModalBaseElement, umbOpenModal } from '@umbraco-cms/backoffice/modal import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; -const elementName = 'umb-restore-from-recycle-bin-modal'; - -@customElement(elementName) +@customElement('umb-restore-from-recycle-bin-modal') export class UmbRestoreFromRecycleBinModalElement extends UmbModalBaseElement< UmbRestoreFromRecycleBinModalData, UmbRestoreFromRecycleBinModalValue @@ -21,10 +19,13 @@ export class UmbRestoreFromRecycleBinModalElement extends UmbModalBaseElement< private _isAutomaticRestore = false; @state() - private _restoreItem?: any; + private _destinationItem?: any; @state() - private _destinationItem?: any; + private _destinationItemName?: string; + + @state() + private _restoreItemName?: string; #recycleBinRepository?: UmbRecycleBinRepository; @@ -34,7 +35,16 @@ export class UmbRestoreFromRecycleBinModalElement extends UmbModalBaseElement< super.firstUpdated(_changedProperties); if (!this.data?.unique) throw new Error('Cannot restore an item without a unique identifier.'); - this._restoreItem = await this.#requestItem(this.data.unique); + const restoreItem = await this.#requestItem(this.data.unique); + + if (this.data.itemDataResolver) { + const resolver = new this.data.itemDataResolver(this); + resolver.setData(restoreItem); + this._restoreItemName = await resolver.getName(); + } else { + this._restoreItemName = restoreItem.name; + } + const unique = await this.#requestAutomaticRestoreDestination(); if (unique !== undefined) { @@ -47,9 +57,7 @@ export class UmbRestoreFromRecycleBinModalElement extends UmbModalBaseElement< // TODO: handle ROOT lookup. Currently, we can't look up the root in the item repository. // This is a temp solution to show something in the UI. if (unique === null) { - this._destinationItem = { - name: 'ROOT', - }; + this._destinationItemName = 'Root'; this.#setDestinationValue({ unique: null, @@ -61,6 +69,14 @@ export class UmbRestoreFromRecycleBinModalElement extends UmbModalBaseElement< this._destinationItem = await this.#requestItem(unique); if (!this._destinationItem) throw new Error('Cant find destination item.'); + if (this.data?.itemDataResolver) { + const resolver = new this.data.itemDataResolver(this); + resolver.setData(this._destinationItem); + this._destinationItemName = await resolver.getName(); + } else { + this._destinationItemName = this._destinationItem.name; + } + this.#setDestinationValue({ unique: this._destinationItem.unique, entityType: this._destinationItem.entityType, @@ -138,7 +154,7 @@ export class UmbRestoreFromRecycleBinModalElement extends UmbModalBaseElement< ${this._isAutomaticRestore - ? html` Restore ${this._restoreItem?.name} to ${this._destinationItem?.name}` + ? html` Restore ${this._restoreItemName} to ${this._destinationItemName}` : this.#renderCustomSelectDestination()} ${this.#renderActions()} @@ -151,8 +167,8 @@ export class UmbRestoreFromRecycleBinModalElement extends UmbModalBaseElement<

Cannot automatically restore this item.

There is no location where this item can be automatically restored. You can select a new location below.

Restore to:
- ${this._destinationItem - ? html` + ${this._destinationItem && this._destinationItemName + ? html` (this._destinationItem = undefined)} label="Remove" >${this.localize.term('general_remove')}, UmbPickerModalValue> | string; } 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 63dc760e12..e4141ff231 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 @@ -24,6 +24,7 @@ export class UmbRestoreFromRecycleBinEntityAction extends UmbEntityActionBase, UmbPickerModalValue> | string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts index bbcf5e5185..25a428c959 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts @@ -46,6 +46,7 @@ export const manifests: Array = [ forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { itemRepositoryAlias: UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, + itemDataResolver: UmbDocumentItemDataResolver, recycleBinRepositoryAlias: UMB_DOCUMENT_RECYCLE_BIN_REPOSITORY_ALIAS, pickerModal: UMB_DOCUMENT_PICKER_MODAL, }, From 63ae29a1aa6df0b482b453e62fdaa35267e13aed Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:48:55 +0200 Subject: [PATCH 2/2] Preview: Fixes a potential issue where the preview URL could be a different backoffice host (#20591) * hotfix: ensures that local urls stay relative so we land up on the correct backoffice host that the user initiated the preview session from originally * feat: since ensureAbsoluteUrl is never supplied anymore, we can remove the parameter altogether * Remove unused dependency * Expose IsExternal for URLs * feat: adds localize controller * chore: generates api models * feat: marks the internal preview default url as relative, so that the `` tag is taken into consideration - that way the URL will open on whatever host is active * Remove IsExternal from the API again * regenerate types --------- Co-authored-by: kjac --- .../Factories/DocumentUrlFactory.cs | 13 ++++--------- src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs | 4 ++-- .../workspace/document-workspace.context.ts | 8 +++++++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs index 998eb80873..53bd255a2b 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs @@ -16,7 +16,6 @@ public class DocumentUrlFactory : IDocumentUrlFactory private readonly UrlProviderCollection _urlProviders; private readonly IPreviewService _previewService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IAbsoluteUrlBuilder _absoluteUrlBuilder; private readonly ILogger _logger; public DocumentUrlFactory( @@ -24,14 +23,12 @@ public class DocumentUrlFactory : IDocumentUrlFactory UrlProviderCollection urlProviders, IPreviewService previewService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IAbsoluteUrlBuilder absoluteUrlBuilder, ILogger logger) { _publishedUrlInfoProvider = publishedUrlInfoProvider; _urlProviders = urlProviders; _previewService = previewService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _absoluteUrlBuilder = absoluteUrlBuilder; _logger = logger; } @@ -39,7 +36,7 @@ public class DocumentUrlFactory : IDocumentUrlFactory { ISet urlInfos = await _publishedUrlInfoProvider.GetAllAsync(content); return urlInfos - .Select(urlInfo => CreateDocumentUrlInfo(urlInfo, false)) + .Select(CreateDocumentUrlInfo) .ToArray(); } @@ -89,18 +86,16 @@ public class DocumentUrlFactory : IDocumentUrlFactory } } - return CreateDocumentUrlInfo(previewUrlInfo, previewUrlInfo.IsExternal is false); + return CreateDocumentUrlInfo(previewUrlInfo); } - private DocumentUrlInfo CreateDocumentUrlInfo(UrlInfo urlInfo, bool ensureAbsoluteUrl) + private DocumentUrlInfo CreateDocumentUrlInfo(UrlInfo urlInfo) { var url = urlInfo.Url?.ToString(); return new DocumentUrlInfo { Culture = urlInfo.Culture, - Url = ensureAbsoluteUrl && url is not null - ? _absoluteUrlBuilder.ToAbsoluteUrl(url).ToString() - : url, + Url = url, Message = urlInfo.Message, Provider = urlInfo.Provider, }; diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs index c4385bb5ad..e6d4def79e 100644 --- a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs @@ -144,7 +144,7 @@ public class NewDefaultUrlProvider : IUrlProvider public Task GetPreviewUrlAsync(IContent content, string? culture, string? segment) => Task.FromResult( UrlInfo.AsUrl( - $"/{Constants.System.UmbracoPathSegment}/preview?id={content.Key}&culture={culture}&segment={segment}", + $"preview?id={content.Key}&culture={culture}&segment={segment}", Alias, culture, isExternal: false)); @@ -182,7 +182,7 @@ public class NewDefaultUrlProvider : IUrlProvider // We have the published content now, so we can check if the culture is published, and thus avoid the DB hit. string route; var isDraft = _umbracoContextAccessor.GetRequiredUmbracoContext().InPreviewMode; - if(isDraft is false && string.IsNullOrWhiteSpace(culture) is false && content.Cultures.Any() && content.IsInvariantOrHasCulture(culture) is false) + if (isDraft is false && string.IsNullOrWhiteSpace(culture) is false && content.Cultures.Any() && content.IsInvariantOrHasCulture(culture) is false) { route = "#"; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 9eef3cde1b..0d199d874c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -40,6 +40,7 @@ import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/documen import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import type { UmbVariantPropertyGuardRule } from '@umbraco-cms/backoffice/property'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; type ContentModel = UmbDocumentDetailModel; type ContentTypeModel = UmbDocumentTypeDetailModel; @@ -70,6 +71,7 @@ export class UmbDocumentWorkspaceContext #isTrashedContext = new UmbIsTrashedEntityContext(this); #documentSegmentRepository = new UmbDocumentSegmentRepository(this); #actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; + #localize = new UmbLocalizationController(this); constructor(host: UmbControllerHost) { super(host, { @@ -350,7 +352,11 @@ export class UmbDocumentWorkspaceContext } if (previewUrlData.message) { - umbPeekError(this._host, { color: 'danger', headline: 'Preview error', message: previewUrlData.message }); + umbPeekError(this._host, { + color: 'danger', + headline: this.#localize.term('general_preview'), + message: previewUrlData.message, + }); throw new Error(previewUrlData.message); } }