From ab51aac5c64250ab785dde233af322d95f8669e4 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:57:24 +0100 Subject: [PATCH] Backoffice Item Pickers: Show error for missing items in 10 picker types (closes #19329, #20270, #20367) (#20762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add errorDetail property to umb-entity-item-ref Add optional errorDetail property to display additional context (such as file paths or IDs) in error states. This enhances the error display to show both the error message and relevant details. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Make _removeItem protected in UmbPickerInputContext Change #removeItem from private to protected to allow subclasses to reuse the removal logic while customizing the confirmation dialog. This enables better extensibility for specialized picker contexts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix static file picker to show error state for missing files Update umb-input-static-file to observe statuses and render based on item state (loading, error, success). When a static file is missing (API returns empty array), displays error state with alert icon and file path detail using umb-entity-item-ref. Also adds standalone property support for proper single-item styling. Fixes #19329 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Show file path in static file remove confirmation dialog Override requestRemoveItem in UmbStaticFilePickerInputContext to display the file path instead of "Not found" in the confirmation dialog when removing missing static files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Show GUID in document picker error state Display the document GUID as errorDetail when a document is not found (deleted/gone). This provides useful context for editors to identify which document was referenced. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Show GUID in document picker remove confirmation dialog Display the document GUID instead of "Not found" in the remove confirmation dialog when the document no longer exists. This provides useful context for editors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix: apply the temp model which the context uses * Refactor: Move requestRemoveItem logic to base UmbPickerInputContext Eliminated duplicate code across three picker contexts by: - Adding protected getItemDisplayName() method to base class - Moving requestRemoveItem implementation to base class - Removing duplicate implementations from document, member, and static file pickers - Static file picker overrides getItemDisplayName() to show file path Net reduction: 19 lines of code (69 removed, 50 added) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Document Type Picker: Show error state for missing items (fixes #20367) Apply the same error state handling to the document type picker that was implemented for static files, documents, and members. When a referenced document type is missing or deleted: - Show error state with the GUID as errorDetail - Allow removal with proper confirmation dialog - Use umb-entity-item-ref for error display - Use uui-ref-node-document-type for successful items 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Additional pickers: Show error states for missing items in user, language, media-type, member-type, member-group, and user-group pickers Apply the same error state handling pattern to six additional picker types: - user-input: Users - input-language: Languages - input-media-type: Media types - input-member-type: Member types - input-member-group: Member groups - user-group-input: User groups All pickers now: - Observe statuses from UmbRepositoryItemsManager - Show error state with GUID when referenced item is missing/deleted - Use umb-entity-item-ref for error display - Use specialized components (uui-ref-node, umb-user-group-ref, etc.) for successful items - Allow removal with proper confirmation dialog showing GUID Maintains code reusability by using the base class requestRemoveItem method with getItemDisplayName() for consistent error handling across all pickers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Lint: Remove unused 'when' imports from input-media-type and user-group-input * Refactor: Add #renderItem helper method to all pickers for consistency - Add #renderItem to user-input (extracted from inline repeat callback) - Change _renderItem to #renderItem in user-group-input for consistency - Change _renderItem to #renderItem in input-static-file for consistency All 10 pickers now use consistent #renderItem helper method pattern, improving code readability and maintainability as suggested by @nielslyngsoe * `import` sorting * Corrected (old) JSDoc typos * Markup tidy-up * exported `UmbPropertyEditorUIStaticFilePickerElement` as `element` --------- Co-authored-by: Claude Co-authored-by: leekelleher --- .../entity-item-ref.element.ts | 53 ++++++----- .../core/picker-input/picker-input.context.ts | 25 +++-- .../input-document-type.element.ts | 95 +++++++++++++------ .../input-document/input-document.element.ts | 40 ++++---- .../input-language/input-language.element.ts | 60 +++++++----- .../input-media-type.element.ts | 71 ++++++++++---- .../input-member-group.element.ts | 95 +++++++++++++------ .../input-member-type.element.ts | 50 ++++++++-- .../input-member/input-member.element.ts | 60 +++++++----- .../input-static-file.context.ts | 10 +- .../input-static-file.element.ts | 37 +++++--- .../static-file-picker/manifests.ts | 2 +- ...ty-editor-ui-static-file-picker.element.ts | 2 + .../user-group-input.element.ts | 62 ++++++++++-- .../user-input/user-input.element.ts | 35 +++++-- 15 files changed, 481 insertions(+), 216 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts index 97b469d8a5..836007d29c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts @@ -1,15 +1,16 @@ import type { ManifestEntityItemRef } from './entity-item-ref.extension.js'; -import { customElement, property, type PropertyValueMap, state, css, html } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; -import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; import { UUIBlinkAnimationValue } from '@umbraco-cms/backoffice/external/uui'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import './default-item-ref.element.js'; -import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; @customElement('umb-entity-item-ref') export class UmbEntityItemRefElement extends UmbLitElement { @@ -20,9 +21,6 @@ export class UmbEntityItemRefElement extends UmbLitElement { private _component?: any; // TODO: Add type @property({ type: Object, attribute: false }) - public get item(): UmbEntityModel | undefined { - return this.#item; - } public set item(value: UmbEntityModel | undefined) { const oldValue = this.#item; this.#item = value; @@ -41,6 +39,9 @@ export class UmbEntityItemRefElement extends UmbLitElement { // If the component is already created, but the entity type is different, we need to destroy the component. this.#createController(value.entityType); } + public get item(): UmbEntityModel | undefined { + return this.#item; + } #readonly = false; @property({ type: Boolean, reflect: true }) @@ -124,20 +125,23 @@ export class UmbEntityItemRefElement extends UmbLitElement { error?: boolean; @property({ type: String, attribute: 'error-message', reflect: false }) - errorMessage?: string; + errorMessage?: string | null; + + @property({ type: String, attribute: 'error-detail', reflect: false }) + errorDetail?: string | null; #pathAddendum = new UmbRoutePathAddendumContext(this); #onSelected(event: UmbSelectedEvent) { event.stopPropagation(); - const unique = this.#item?.unique; + const unique = this.item?.unique; if (!unique) throw new Error('No unique id found for item'); this.dispatchEvent(new UmbSelectedEvent(unique)); } #onDeselected(event: UmbDeselectedEvent) { event.stopPropagation(); - const unique = this.#item?.unique; + const unique = this.item?.unique; if (!unique) throw new Error('No unique id found for item'); this.dispatchEvent(new UmbDeselectedEvent(unique)); } @@ -163,7 +167,7 @@ export class UmbEntityItemRefElement extends UmbLitElement { // TODO: I would say this code can use feature of the UmbExtensionsElementInitializer, to set properties and get a fallback element. [NL] // assign the properties to the component - component.item = this.#item; + component.item = this.item; component.readonly = this.readonly; component.standalone = this.standalone; component.selectOnly = this.selectOnly; @@ -192,20 +196,25 @@ export class UmbEntityItemRefElement extends UmbLitElement { if (this._component) { return html`${this._component}`; } + // Error: if (this.error) { - return html` - - - `; + return html` + + + + + `; } + // Loading: return html``; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index 2829e753db..9c90d216c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -31,8 +31,8 @@ export class UmbPickerInputContext< public readonly interactionMemory = new UmbInteractionMemoryManager(this); /** - * Define a minimum amount of selected items in this input, for this input to be valid. - * @returns {number} The minimum number of items required. + * Define a maximum amount of selected items in this input, for this input to be valid. + * @returns {number} The maximum number of items required. */ public get max() { return this._max; @@ -43,7 +43,7 @@ export class UmbPickerInputContext< private _max = Infinity; /** - * Define a maximum amount of selected items in this input, for this input to be valid. + * Define a minimum amount of selected items in this input, for this input to be valid. * @returns {number} The minimum number of items required. */ public get min() { @@ -111,21 +111,32 @@ export class UmbPickerInputContext< this.getHostElement().dispatchEvent(new UmbChangeEvent()); } + /** + * Get the display name for an item to show in the remove confirmation dialog. + * Subclasses can override this to provide custom formatting for missing items. + * @param item - The item to get the display name for, or undefined if not found + * @param unique - The unique identifier of the item + * @returns The display name to show in the dialog + */ + protected getItemDisplayName(item: PickedItemType | undefined, unique: string): string { + return item?.name ?? unique; + } + async requestRemoveItem(unique: string) { const item = this.#itemManager.getItems().find((item) => item.unique === unique); + const name = this.getItemDisplayName(item, unique); - const name = item?.name ?? '#general_notFound'; await umbConfirmModal(this, { color: 'danger', - headline: `#actions_remove ${name}?`, + headline: `#actions_remove?`, content: `#defaultdialogs_confirmremove ${name}?`, confirmLabel: '#actions_remove', }); - this.#removeItem(unique); + this._removeItem(unique); } - #removeItem(unique: string) { + protected _removeItem(unique: string) { const newSelection = this.getSelection().filter((value) => value !== unique); this.setSelection(newSelection); this.getHostElement().dispatchEvent(new UmbChangeEvent()); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts index 01f74f88bc..cbff030a4c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts @@ -2,13 +2,16 @@ import type { UmbDocumentTypeItemModel, UmbDocumentTypeTreeItemModel } from '../ import { UMB_DOCUMENT_TYPE_WORKSPACE_MODAL } from '../../constants.js'; import { UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN } from '../../paths.js'; import { UmbDocumentTypePickerInputContext } from './input-document-type.context.js'; -import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-document-type') export class UmbInputDocumentTypeElement extends UmbFormControlMixin( @@ -112,6 +115,9 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + @state() private _editPath = ''; @@ -143,6 +149,7 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -151,8 +158,8 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin !x.isFolder && x.isElement === false; } @@ -184,8 +191,8 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + ${when( + !this.readonly, + () => html` + + this.#removeItem(unique)}> + + `, + )} + + `; + } + + // For successful items, use the document type specific component + if (!item) return nothing; + const href = this._editPath + UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique }); + return html` + + ${this.#renderIcon(item)} + + ${when( + !this.readonly, + () => html` + this.#removeItem(unique)}> + `, + )} + + + `; + }, )} `; } - #renderItem(item: UmbDocumentTypeItemModel) { - if (!item.unique) return; - const href = this._editPath + UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique: item.unique }); - return html` - - ${this.#renderIcon(item)} - - ${when( - !this.readonly, - () => html` - this.#removeItem(item)}> - `, - )} - - - `; - } - #renderIcon(item: UmbDocumentTypeItemModel) { if (!item.icon) return; return html``; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index 516251a4b5..302d9594c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -239,24 +239,28 @@ export class UmbInputDocumentElement extends UmbFormControlMixin { const unique = status.unique; const item = this._items?.find((x) => x.unique === unique); - return html` - ${when( - !this.readonly, - () => html` - - this.#onRemove(unique)}> - - `, - )} - `; + const isError = status.state.type === 'error'; + return html` + + ${when( + !this.readonly, + () => html` + + this.#onRemove(unique)}> + + `, + )} + + `; }, )} diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts index 826bbe0b83..9a46266943 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.element.ts @@ -6,6 +6,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; @customElement('umb-input-language') export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -17,7 +18,7 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, return modelEntry; }, identifier: 'Umb.SorterIdentifier.InputLanguage', - itemSelector: 'uui-ref-node', + itemSelector: 'umb-entity-item-ref', containerSelector: 'uui-ref-list', onChange: ({ model }) => { this.selection = model; @@ -115,6 +116,9 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, @state() private _items: Array = []; + @state() + private _statuses?: Array; + #pickerContext = new UmbLanguagePickerInputContext(this); constructor() { @@ -134,6 +138,7 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -147,8 +152,8 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, }); } - #onRemove(item: UmbLanguageItemModel) { - this.#pickerContext.requestRemoveItem(item.unique); + #onRemove(unique: string) { + this.#pickerContext.requestRemoveItem(unique); } override render() { @@ -167,29 +172,38 @@ export class UmbInputLanguageElement extends UUIFormControlMixin(UmbLitElement, } #renderItems() { - if (!this._items) return; + if (!this._statuses) return; return html` ${repeat( - this._items, - (item) => item.unique, - (item) => - html` - ${when( - !this.readonly, - () => html` - - this.#onRemove(item)}> - - `, - )} - `, + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + return html` + + ${when( + !this.readonly, + () => html` + + this.#onRemove(unique)}> + + `, + )} + + `; + }, )} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts index cd659f441a..d9e7949db0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts @@ -8,6 +8,9 @@ import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-media-type') export class UmbInputMediaTypeElement extends UmbFormControlMixin( @@ -95,6 +98,9 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + @state() private _editPath = ''; @@ -126,6 +132,7 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -138,8 +145,8 @@ export class UmbInputMediaTypeElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + + this.#removeItem(unique)}> + + + `; + } + + // For successful items, use the media type specific component + if (!item) return nothing; + const href = `${this._editPath}edit/${unique}`; + return html` + + ${this.#renderIcon(item)} + + + this.#removeItem(unique)}> + + + `; + }, )} `; } - #renderItem(item: UmbMediaTypeItemModel) { - if (!item.unique) return; - const href = `${this._editPath}edit/${item.unique}`; - return html` - - ${this.#renderIcon(item)} - - - this.#removeItem(item)} label=${this.localize.term('general_remove')}> - - - `; - } - #renderIcon(item: UmbMediaTypeItemModel) { if (!item.icon) return; return html``; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts index bec7d38b13..40a8c9e3d5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts @@ -1,13 +1,16 @@ import type { UmbMemberGroupItemModel } from '../../types.js'; import { UmbMemberGroupPickerInputContext } from './input-member-group.context.js'; -import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-member-group') export class UmbInputMemberGroupElement extends UmbFormControlMixin( @@ -124,6 +127,9 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + #pickerContext = new UmbMemberGroupPickerInputContext(this); constructor() { @@ -152,6 +158,7 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observeItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -164,8 +171,8 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + ${when( + !this.readonly, + () => html` + + this.#removeItem(unique)}> + + `, + )} + + `; + } + + // For successful items, use uui-ref-node + if (!item) return nothing; + return html` + + + ${when( + !this.readonly, + () => + html` this.#removeItem(unique)} + label=${this.localize.term('general_remove')}>`, + )} + + + + `; + }, )} `; @@ -199,27 +255,6 @@ export class UmbInputMemberGroupElement extends UmbFormControlMixin - ${this.#renderRemoveButton(item)} - - - `; - } - - #renderRemoveButton(item: UmbMemberGroupItemModel) { - if (this.readonly) return nothing; - return html` this.#removeItem(item)} - label=${this.localize.term('general_remove')}>`; - } - static override styles = [ css` #btn-add { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts index dd887b2d66..6386a906c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.element.ts @@ -1,9 +1,12 @@ import { UmbMemberTypePickerInputContext } from './input-member-type.context.js'; -import { css, html, customElement, property, state, repeat, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbUniqueItemModel } from '@umbraco-cms/backoffice/models'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; +import type { UmbUniqueItemModel } from '@umbraco-cms/backoffice/models'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-member-type') export class UmbInputMemberTypeElement extends UmbFormControlMixin( @@ -73,6 +76,9 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + #pickerContext = new UmbMemberTypePickerInputContext(this); constructor() { @@ -92,6 +98,7 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin (this.value = selection.join(','))); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -109,13 +116,13 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => this.#renderItem(status), )} `; @@ -134,14 +141,37 @@ export class UmbInputMemberTypeElement extends UmbFormControlMixin x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + + this.#pickerContext.requestRemoveItem(unique)}> + + + `; + } + + // For successful items, use the member type specific component + if (!item?.unique) return nothing; return html` ${when(item.icon, () => html``)} this.#pickerContext.requestRemoveItem(item.unique!)} + @click=${() => this.#pickerContext.requestRemoveItem(unique)} label="Remove Member Type ${item.name}" >${this.localize.term('general_remove')} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts index d5b60d9e04..8ddf6e6068 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts @@ -1,12 +1,13 @@ import type { UmbMemberItemModel } from '../../item/types.js'; import { UmbMemberPickerInputContext } from './input-member.context.js'; -import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/member-type'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; @customElement('umb-input-member') export class UmbInputMemberElement extends UmbFormControlMixin( @@ -121,6 +122,9 @@ export class UmbInputMemberElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + #pickerContext = new UmbMemberPickerInputContext(this); constructor() { @@ -140,6 +144,7 @@ export class UmbInputMemberElement extends UmbFormControlMixin (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observeItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } #openPicker() { @@ -156,8 +161,8 @@ export class UmbInputMemberElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; + return html` + + ${when( + !this.readonly, + () => html` + + this.#onRemove(unique)}> + + `, + )} + + `; + }, )} `; } - #renderItem(item: UmbMemberItemModel) { - if (!item.unique) return nothing; - return html` - - ${this.#renderRemoveButton(item)} - - `; - } - #renderAddButton() { if (this.selection.length >= this.max) return nothing; if (this.readonly && this.selection.length > 0) { @@ -202,13 +223,6 @@ export class UmbInputMemberElement extends UmbFormControlMixin this.#onRemove(item)} label=${this.localize.term('general_remove')}> - `; - } - static override styles = [ css` #btn-add { diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.context.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.context.ts index 7a6bc3826a..75bbb7e31c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.context.ts @@ -1,8 +1,9 @@ import { UMB_STATIC_FILE_PICKER_MODAL } from '../../modals/index.js'; -import type { UmbStaticFilePickerModalData, UmbStaticFilePickerModalValue } from '../../modals/index.js'; import { UMB_STATIC_FILE_ITEM_REPOSITORY_ALIAS } from '../../constants.js'; +import type { UmbStaticFilePickerModalData, UmbStaticFilePickerModalValue } from '../../modals/index.js'; import type { UmbStaticFileItemModel } from '../../types.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbStaticFilePickerInputContext extends UmbPickerInputContext< @@ -11,7 +12,14 @@ export class UmbStaticFilePickerInputContext extends UmbPickerInputContext< UmbStaticFilePickerModalData, UmbStaticFilePickerModalValue > { + #serializer = new UmbServerFilePathUniqueSerializer(); + constructor(host: UmbControllerHost) { super(host, UMB_STATIC_FILE_ITEM_REPOSITORY_ALIAS, UMB_STATIC_FILE_PICKER_MODAL); } + + protected override getItemDisplayName(item: UmbStaticFileItemModel | undefined, unique: string): string { + // If item doesn't exist, use the file path as the name + return item?.name ?? this.#serializer.toServerPath(unique) ?? unique; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts index 9e42a0b6f7..e533364de4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/components/input-static-file/input-static-file.element.ts @@ -2,9 +2,12 @@ import type { UmbStaticFileItemModel } from '../../repository/item/types.js'; import { UmbStaticFilePickerInputContext } from './input-static-file.context.js'; import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-static-file') export class UmbInputStaticFileElement extends UmbFormControlMixin( @@ -79,6 +82,9 @@ export class UmbInputStaticFileElement extends UmbFormControlMixin; + @state() + private _statuses?: Array; + #pickerContext = new UmbStaticFilePickerInputContext(this); constructor() { @@ -98,6 +104,7 @@ export class UmbInputStaticFileElement extends UmbFormControlMixin (this.value = selection.join(','))); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses)); } protected override getFormElement() { @@ -105,13 +112,13 @@ export class UmbInputStaticFileElement extends UmbFormControlMixin ${repeat( - this._items, - (item) => item.unique, - (item) => this._renderItem(item), + this._statuses, + (status) => status.unique, + (status) => this.#renderItem(status), )} ${this.#renderAddButton()} @@ -137,17 +144,25 @@ export class UmbInputStaticFileElement extends UmbFormControlMixin x.unique === unique); + const isError = status.state.type === 'error'; + return html` - - + this.#pickerContext.requestRemoveItem(item.unique)}> + @click=${() => this.#pickerContext.requestRemoveItem(unique)}> - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/manifests.ts index cd9c2b9c64..261047fb15 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/manifests.ts @@ -4,7 +4,7 @@ export const manifest: ManifestPropertyEditorUi = { type: 'propertyEditorUi', alias: 'Umb.PropertyEditorUi.StaticFilePicker', name: 'Static File Picker Property Editor UI', - js: () => import('./property-editor-ui-static-file-picker.element.js'), + element: () => import('./property-editor-ui-static-file-picker.element.js'), meta: { label: 'Static File Picker', icon: 'icon-document', diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/property-editor-ui-static-file-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/property-editor-ui-static-file-picker.element.ts index 81cfbda18d..d5098b7700 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/property-editor-ui-static-file-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/property-editors/static-file-picker/property-editor-ui-static-file-picker.element.ts @@ -73,6 +73,8 @@ export class UmbPropertyEditorUIStaticFilePickerElement extends UmbLitElement im } } +export { UmbPropertyEditorUIStaticFilePickerElement as element }; + export default UmbPropertyEditorUIStaticFilePickerElement; declare global { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts index f509aee058..d86884dc90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/input-user-group/user-group-input.element.ts @@ -1,12 +1,24 @@ import { UMB_USER_GROUP_ENTITY_TYPE } from '../../entity.js'; import type { UmbUserGroupItemModel } from '../../repository/index.js'; import { UmbUserGroupPickerInputContext } from './user-group-input.context.js'; -import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; +import { + css, + customElement, + html, + ifDefined, + nothing, + property, + repeat, + state, +} from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; +import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; +import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +import '@umbraco-cms/backoffice/entity-item'; @customElement('umb-user-group-input') export class UmbUserGroupInputElement extends UUIFormControlMixin(UmbLitElement, '') { @@ -75,6 +87,9 @@ export class UmbUserGroupInputElement extends UUIFormControlMixin(UmbLitElement, @state() private _items?: Array; + @state() + private _statuses?: Array; + #pickerContext = new UmbUserGroupPickerInputContext(this); @state() @@ -97,6 +112,7 @@ export class UmbUserGroupInputElement extends UUIFormControlMixin(UmbLitElement, this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) .addAdditionalPath(UMB_USER_GROUP_ENTITY_TYPE) @@ -114,7 +130,15 @@ export class UmbUserGroupInputElement extends UUIFormControlMixin(UmbLitElement, override render() { return html` - ${this._items?.map((item) => this._renderItem(item))} + + ${this._statuses + ? repeat( + this._statuses, + (status) => status.unique, + (status) => this.#renderItem(status), + ) + : nothing} + x.unique === unique); + const isError = status.state.type === 'error'; + + // For error state, use umb-entity-item-ref + if (isError) { + return html` + + + this.#pickerContext.requestRemoveItem(unique)}> + + + `; + } + + // For successful items, use umb-user-group-ref + if (!item?.unique) return nothing; + const href = `${this._editUserGroupPath}edit/${unique}`; return html` ${item.icon ? html`` : nothing} this.#pickerContext.requestRemoveItem(item.unique)} + @click=${() => this.#pickerContext.requestRemoveItem(unique)} label=${this.localize.term('general_remove')}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts index f562192410..a1de877671 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.element.ts @@ -6,6 +6,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; // TODO: Shall we rename to 'umb-input-user'? [LK] @customElement('umb-user-input') @@ -92,6 +93,9 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') @state() private _items?: Array; + @state() + private _statuses?: Array; + #pickerContext = new UmbUserPickerInputContext(this); constructor() { @@ -111,6 +115,7 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe(this.#pickerContext.statuses, (statuses) => (this._statuses = statuses), '_observeStatuses'); } protected override getFormElement() { @@ -121,8 +126,8 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') this.#pickerContext.openPicker({}); } - #removeItem(item: UmbUserItemModel) { - this.#pickerContext.requestRemoveItem(item.unique); + #removeItem(unique: string) { + this.#pickerContext.requestRemoveItem(unique); } override render() { @@ -141,24 +146,34 @@ export class UmbUserInputElement extends UUIFormControlMixin(UmbLitElement, '') } #renderItems() { - if (!this._items) return nothing; + if (!this._statuses) return nothing; return html` ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderItem(item), + this._statuses, + (status) => status.unique, + (status) => this.#renderItem(status), )} `; } - #renderItem(item: UmbUserItemModel) { - if (!item.unique) return nothing; + #renderItem(status: UmbRepositoryItemsStatus) { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + const isError = status.state.type === 'error'; return html` - + - this.#removeItem(item)}> + this.#removeItem(unique)}> `;