From 3e3139f9d5a416d90263c1a0007d1578cf742658 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 7 Jul 2025 07:30:51 +0200 Subject: [PATCH 1/9] Populate name for content and media on URL picker if title is left empty. --- .../MultiUrlPickerValueConverter.cs | 5 ++ .../input-multi-url.element.ts | 63 +++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs index a520fa35ef0a..4b285caf1cd4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs @@ -101,6 +101,11 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType continue; } + if (string.IsNullOrEmpty(dto.Name)) + { + dto.Name = content.Name; + } + url = content.Url(_publishedUrlProvider); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index f7e23b623102..ef80c6b9644f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -21,8 +21,8 @@ import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; -import { UmbDocumentUrlRepository, UmbDocumentUrlsDataResolver } from '@umbraco-cms/backoffice/document'; -import { UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media'; +import { UmbDocumentItemRepository, UmbDocumentUrlRepository, UmbDocumentUrlsDataResolver } from '@umbraco-cms/backoffice/document'; +import { UmbMediaItemRepository, UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media'; /** * @element umb-input-multi-url @@ -131,7 +131,7 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, this.#urls = [...data]; // Unfreeze data coming from State, so we can manipulate it. super.value = this.#urls.map((x) => x.url).join(','); this.#sorter.setModel(this.#urls); - this.#populateLinksUrl(); + this.#populateLinksNameAndUrl(); } get urls(): Array { return this.#urls; @@ -139,6 +139,13 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, #urls: Array = []; + #documentUrlRepository = new UmbDocumentUrlRepository(this); + #mediaUrlRepository = new UmbMediaUrlRepository(this); + #documentItemRepository = new UmbDocumentItemRepository(this); + #mediaItemRepository = new UmbMediaItemRepository(this); + + #documentUrlsDataResolver = new UmbDocumentUrlsDataResolver(this); + /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. * @type {boolean} @@ -163,6 +170,9 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, @state() private _modalRoute?: UmbModalRouteBuilder; + @state() + _resolvedLinkNames: Array<{ unique: string; name: string }> = []; + @state() _resolvedLinkUrls: Array<{ unique: string; url: string }> = []; @@ -235,19 +245,26 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, }); } - #populateLinksUrl() { + #populateLinksNameAndUrl() { // Documents and media have URLs saved in the local link format. Display the actual URL to align with what // the user sees when they selected it initially. this.#urls.forEach(async (link) => { if (!link.unique) return; + let name: string | undefined = undefined; let url: string | undefined = undefined; switch (link.type) { case 'document': { + if (!link.name || link.name.length === 0) { + name = await this.#getNameForDocument(link.unique); + } url = await this.#getUrlForDocument(link.unique); break; } case 'media': { + if (!link.name || link.name.length === 0) { + name = await this.#getNameForMedia(link.unique); + } url = await this.#getUrlForMedia(link.unique); break; } @@ -255,6 +272,11 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, break; } + if (name) { + const resolvedName = { unique: link.unique, name }; + this._resolvedLinkNames = [...this._resolvedLinkNames, resolvedName]; + } + if (url) { const resolvedUrl = { unique: link.unique, url }; this._resolvedLinkUrls = [...this._resolvedLinkUrls, resolvedUrl]; @@ -263,21 +285,37 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, } async #getUrlForDocument(unique: string) { - const documentUrlRepository = new UmbDocumentUrlRepository(this); - const { data: documentUrlData } = await documentUrlRepository.requestItems([unique]); + const { data: documentUrlData } = await this.#documentUrlRepository.requestItems([unique]); const urlsItem = documentUrlData?.[0]; - const dataResolver = new UmbDocumentUrlsDataResolver(this); - dataResolver.setData(urlsItem?.urls); - const resolvedUrls = await dataResolver.getUrls(); + + this.#documentUrlsDataResolver.setData(urlsItem?.urls); + const resolvedUrls = await this.#documentUrlsDataResolver.getUrls(); return resolvedUrls?.[0]?.url ?? ''; } async #getUrlForMedia(unique: string) { - const mediaUrlRepository = new UmbMediaUrlRepository(this); - const { data: mediaUrlData } = await mediaUrlRepository.requestItems([unique]); + const { data: mediaUrlData } = await this.#mediaUrlRepository.requestItems([unique]); return mediaUrlData?.[0].url ?? ''; } + async #getNameForDocument(unique: string) { + const { data } = await this.#documentItemRepository.requestItems([unique]); + if (data && data.length > 0) { + return data[0].name; + } + + return ''; + } + + async #getNameForMedia(unique: string) { + const { data } = await this.#mediaItemRepository.requestItems([unique]); + if (data && data.length > 0) { + return data[0].name; + } + + return ''; + } + async #requestRemoveItem(index: number) { const item = this.#urls[index]; if (!item) throw new Error('Could not find item at index: ' + index); @@ -356,12 +394,13 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, #renderItem(link: UmbLinkPickerLink, index: number) { const unique = this.#getUnique(link); const href = this.readonly ? undefined : (this._modalRoute?.({ index }) ?? undefined); + const resolvedName = this._resolvedLinkNames.find((name) => name.unique === link.unique)?.name ?? ''; const resolvedUrl = this._resolvedLinkUrls.find((url) => url.unique === link.unique)?.url ?? ''; return html` From 3a081917a29ca9ecf58c0322e0518cbd2fe305f9 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 7 Jul 2025 07:47:31 +0200 Subject: [PATCH 2/9] Display URL for manually entered URLs. --- .../components/input-multi-url/input-multi-url.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index ef80c6b9644f..27846f3c6479 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -395,7 +395,7 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, const unique = this.#getUnique(link); const href = this.readonly ? undefined : (this._modalRoute?.({ index }) ?? undefined); const resolvedName = this._resolvedLinkNames.find((name) => name.unique === link.unique)?.name ?? ''; - const resolvedUrl = this._resolvedLinkUrls.find((url) => url.unique === link.unique)?.url ?? ''; + const resolvedUrl = this._resolvedLinkUrls.find((url) => url.unique === link.unique)?.url ?? link.url ?? ''; return html` Date: Mon, 7 Jul 2025 07:59:49 +0200 Subject: [PATCH 3/9] Updates from code review. --- .../components/input-multi-url/input-multi-url.element.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index 27846f3c6479..07dd988fd55c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -248,6 +248,8 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, #populateLinksNameAndUrl() { // Documents and media have URLs saved in the local link format. Display the actual URL to align with what // the user sees when they selected it initially. + this._resolvedLinkNames = []; + this._resolvedLinkUrls = [] this.#urls.forEach(async (link) => { if (!link.unique) return; From 851f1aed244f8db3752202c909d3bdd5f1b51958 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 15 Jul 2025 12:35:46 +0100 Subject: [PATCH 4/9] Reverted `elementName` constant --- .../components/input-multi-url/input-multi-url.element.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index 07dd988fd55c..a787d7d97579 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -30,8 +30,7 @@ import { UmbMediaItemRepository, UmbMediaUrlRepository } from '@umbraco-cms/back * @fires blur - when the input loses focus * @fires focus - when the input gains focus */ -const elementName = 'umb-input-multi-url'; -@customElement(elementName) +@customElement('umb-input-multi-url') export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, '') { #sorter = new UmbSorterController(this, { getUniqueOfElement: (element) => { @@ -431,6 +430,6 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, declare global { interface HTMLElementTagNameMap { - [elementName]: UmbInputMultiUrlElement; + 'umb-input-multi-url': UmbInputMultiUrlElement; } } From cc2c0ecf68307881140e768b702122c66f088c2f Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 15 Jul 2025 12:36:03 +0100 Subject: [PATCH 5/9] Sorted imports --- .../input-multi-url/input-multi-url.element.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index a787d7d97579..624bf675fe7c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -12,17 +12,21 @@ import { when, } from '@umbraco-cms/backoffice/external/lit'; import { simpleHashCode } from '@umbraco-cms/backoffice/observable-api'; +import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +import { + UmbDocumentItemRepository, + UmbDocumentUrlRepository, + UmbDocumentUrlsDataResolver, +} from '@umbraco-cms/backoffice/document'; +import { UmbMediaItemRepository, UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; -import { UmbDocumentItemRepository, UmbDocumentUrlRepository, UmbDocumentUrlsDataResolver } from '@umbraco-cms/backoffice/document'; -import { UmbMediaItemRepository, UmbMediaUrlRepository } from '@umbraco-cms/backoffice/media'; /** * @element umb-input-multi-url From 6ace6b2b98e1a7c536bba145b1e5bc35c7959b13 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 15 Jul 2025 12:36:36 +0100 Subject: [PATCH 6/9] Small code tidy-ups --- .../input-multi-url/input-multi-url.element.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index 624bf675fe7c..6ddb90fd94d3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -142,13 +142,13 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, #urls: Array = []; - #documentUrlRepository = new UmbDocumentUrlRepository(this); - #mediaUrlRepository = new UmbMediaUrlRepository(this); #documentItemRepository = new UmbDocumentItemRepository(this); - #mediaItemRepository = new UmbMediaItemRepository(this); - + #documentUrlRepository = new UmbDocumentUrlRepository(this); #documentUrlsDataResolver = new UmbDocumentUrlsDataResolver(this); + #mediaItemRepository = new UmbMediaItemRepository(this); + #mediaUrlRepository = new UmbMediaUrlRepository(this); + /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. * @type {boolean} @@ -249,15 +249,17 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, } #populateLinksNameAndUrl() { - // Documents and media have URLs saved in the local link format. Display the actual URL to align with what - // the user sees when they selected it initially. this._resolvedLinkNames = []; - this._resolvedLinkUrls = [] + this._resolvedLinkUrls = []; + + // Documents and media have URLs saved in the local link format. + // Display the actual URL to align with what the user sees when they selected it initially. this.#urls.forEach(async (link) => { if (!link.unique) return; let name: string | undefined = undefined; let url: string | undefined = undefined; + switch (link.type) { case 'document': { if (!link.name || link.name.length === 0) { From 819f74fbe2234d889ec7242d5a466cee63a3bb02 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 15 Jul 2025 12:38:07 +0100 Subject: [PATCH 7/9] Added logic to render the `url` as the `name` fallback In this case, the `detail` is left empty, giving prominence to the `url` value. --- .../input-multi-url.element.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index 6ddb90fd94d3..8c4370b5f0ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -365,6 +365,17 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, this.dispatchEvent(new UmbChangeEvent()); } + #getResolvedItemName(link: UmbLinkPickerLink): string { + return (link.name || this._resolvedLinkNames.find((name) => name.unique === link.unique)?.name) ?? ''; + } + + #getResolvedItemUrl(link: UmbLinkPickerLink): string { + return ( + (this._resolvedLinkUrls.find((url) => url.unique === link.unique)?.url ?? link.url ?? '') + + (link.queryString || '') + ); + } + override render() { return html`${this.#renderItems()} ${this.#renderAddButton()}`; } @@ -401,14 +412,15 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, #renderItem(link: UmbLinkPickerLink, index: number) { const unique = this.#getUnique(link); const href = this.readonly ? undefined : (this._modalRoute?.({ index }) ?? undefined); - const resolvedName = this._resolvedLinkNames.find((name) => name.unique === link.unique)?.name ?? ''; - const resolvedUrl = this._resolvedLinkUrls.find((url) => url.unique === link.unique)?.url ?? link.url ?? ''; + const name = this.#getResolvedItemName(link); + const url = this.#getResolvedItemUrl(link); + return html` ${when( From b76bacc04395c3859fcde3fd96de14afc11e4955 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 15 Jul 2025 12:39:19 +0100 Subject: [PATCH 8/9] Refactored the get name/url methods for code consistency. --- .../input-multi-url.element.ts | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index 8c4370b5f0ef..8fd27c01e8bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -292,35 +292,27 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, } async #getUrlForDocument(unique: string) { - const { data: documentUrlData } = await this.#documentUrlRepository.requestItems([unique]); - const urlsItem = documentUrlData?.[0]; + const { data: data } = await this.#documentUrlRepository.requestItems([unique]); + + this.#documentUrlsDataResolver.setData(data?.[0]?.urls); - this.#documentUrlsDataResolver.setData(urlsItem?.urls); const resolvedUrls = await this.#documentUrlsDataResolver.getUrls(); return resolvedUrls?.[0]?.url ?? ''; } async #getUrlForMedia(unique: string) { - const { data: mediaUrlData } = await this.#mediaUrlRepository.requestItems([unique]); - return mediaUrlData?.[0].url ?? ''; + const { data } = await this.#mediaUrlRepository.requestItems([unique]); + return data?.[0].url ?? ''; } async #getNameForDocument(unique: string) { const { data } = await this.#documentItemRepository.requestItems([unique]); - if (data && data.length > 0) { - return data[0].name; - } - - return ''; + return data?.[0]?.name ?? ''; } async #getNameForMedia(unique: string) { const { data } = await this.#mediaItemRepository.requestItems([unique]); - if (data && data.length > 0) { - return data[0].name; - } - - return ''; + return data?.[0]?.name ?? ''; } async #requestRemoveItem(index: number) { From e61bbcec91f8499344f455bad07e67846f9da908 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 15 Jul 2025 12:40:41 +0100 Subject: [PATCH 9/9] Updated `#requestRemoveItem()` to use the item's resolved name with a fallback to "item". Also localized the "Remove" button label --- .../input-multi-url/input-multi-url.element.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts index 8fd27c01e8bb..5b9116e45d00 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/components/input-multi-url/input-multi-url.element.ts @@ -315,15 +315,15 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, return data?.[0]?.name ?? ''; } - async #requestRemoveItem(index: number) { + async #requestRemoveItem(index: number, name?: string) { const item = this.#urls[index]; if (!item) throw new Error('Could not find item at index: ' + index); await umbConfirmModal(this, { color: 'danger', - headline: `Remove ${item.name}?`, - content: 'Are you sure you want to remove this item', - confirmLabel: 'Remove', + headline: `Remove ${name || item.name || 'item'}?`, + content: 'Are you sure you want to remove this item?', + confirmLabel: '#general_remove', }); this.#removeItem(index); @@ -421,7 +421,7 @@ export class UmbInputMultiUrlElement extends UUIFormControlMixin(UmbLitElement, this.#requestRemoveItem(index)}> + @click=${() => this.#requestRemoveItem(index, name)}> `, )}