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 97b469d8a5dc..836007d29cd2 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 2829e753db6b..9c90d216c05f 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 01f74f88bce0..cbff030a4c85 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 516251a4b5b9..302d9594c7ee 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 826bbe0b83d6..9a462669437c 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 cd659f441a16..d9e7949db0f0 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 bec7d38b13a7..40a8c9e3d5da 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 dd887b2d66ee..6386a906c1de 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 { 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 { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + +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 d5b60d9e0488..8ddf6e606824 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 7a6bc3826a84..75bbb7e31cfc 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 9e42a0b6f7b1..e533364de471 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 cd9c2b9c6406..261047fb1598 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 81cfbda18ddb..d5098b7700bf 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 f509aee05805..d86884dc9007 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 { + 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 { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; -import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; +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 f5621924100a..a1de877671c4 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)}> `;