From cc1c99bf62f63b187eb9902a13174a7df07c8f53 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 25 Nov 2025 10:10:09 +0100 Subject: [PATCH 01/18] Add entity collection item card extension type + default elements --- .../core/collection/global-components.ts | 2 +- .../default-collection-item-card.element.ts | 52 +++++ .../entity-collection-item-card.element.ts | 183 ++++++++++++++++++ .../entity-collection-item-card.extension.ts | 19 ++ .../global-components.ts | 1 + .../item/entity-collection-item-card/index.ts | 1 + .../item/entity-collection-item-card/types.ts | 1 + .../core/collection/item/global-components.ts | 1 + .../packages/core/collection/item/types.ts | 1 + .../user/user/collection/item/manifests.ts | 11 ++ .../user/user/collection/item/types.ts | 3 + 11 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/global-components.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/global-components.ts index d567a5dbd8ab..125712ca316d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/global-components.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/global-components.ts @@ -1,3 +1,3 @@ import './menu/collection-menu.element.js'; - +import './item/global-components.js'; export * from './menu/collection-menu.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts new file mode 100644 index 000000000000..580a52ff54ea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts @@ -0,0 +1,52 @@ +import type { UmbCollectionItemModel } from '../types.js'; +import { getItemFallbackName, getItemFallbackIcon } from '@umbraco-cms/backoffice/entity-item'; +import { UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import { customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-default-collection-item-card') +export class UmbDefaultCollectionItemCardElement extends UmbLitElement { + @property({ type: Object }) + item?: UmbCollectionItemModel; + + @property({ type: Boolean }) + selectable = false; + + #onSelected(event: CustomEvent) { + if (!this.item) return; + event.stopPropagation(); + this.dispatchEvent(new UmbSelectedEvent(this.item.unique)); + } + + #onDeselected(event: CustomEvent) { + if (!this.item) return; + event.stopPropagation(); + this.dispatchEvent(new UmbSelectedEvent(this.item.unique)); + } + + override render() { + if (!this.item) return nothing; + + return html` + + + ${this.#renderIcon(this.item)} + + `; + } + + #renderIcon(item: UmbCollectionItemModel) { + const icon = item.icon || getItemFallbackIcon(); + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-default-collection-item-card': UmbDefaultCollectionItemCardElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts new file mode 100644 index 000000000000..bc60d2ab3e0a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts @@ -0,0 +1,183 @@ +import type { UmbCollectionItemModel } from '../types.js'; +import type { ManifestEntityCollectionItemCard } from './entity-collection-item-card.extension.js'; +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 { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; +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 './default-collection-item-card.element.js'; + +@customElement('umb-entity-collection-item-card') +export class UmbEntityCollectionItemCardElement extends UmbLitElement { + #extensionsController?: UmbExtensionsElementInitializer; + #item?: UmbCollectionItemModel; + + @state() + private _component?: any; // TODO: Add type + + @property({ type: Object, attribute: false }) + public set item(value: UmbCollectionItemModel | undefined) { + const oldValue = this.#item; + this.#item = value; + + if (value === oldValue) return; + if (!value) return; + + // If the component is already created and the entity type is the same, we can just update the item. + if (this._component && value.entityType === oldValue?.entityType) { + this._component.item = value; + return; + } + + this.#pathAddendum.setAddendum('collection-item-card/' + value.entityType + '/' + value.unique); + + // 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(): UmbCollectionItemModel | undefined { + return this.#item; + } + + #selectable = false; + @property({ type: Boolean, reflect: true }) + public get selectable() { + return this.#selectable; + } + public set selectable(value) { + this.#selectable = value; + + if (this._component) { + this._component.selectable = this.#selectable; + } + } + + #selectOnly = false; + @property({ type: Boolean, attribute: 'select-only', reflect: true }) + public get selectOnly() { + return this.#selectOnly; + } + public set selectOnly(value) { + this.#selectOnly = value; + + if (this._component) { + this._component.selectOnly = this.#selectOnly; + } + } + + #selected = false; + @property({ type: Boolean, reflect: true }) + public get selected() { + return this.#selected; + } + public set selected(value) { + this.#selected = value; + + if (this._component) { + this._component.selected = this.#selected; + } + } + + #disabled = false; + @property({ type: Boolean, reflect: true }) + public get disabled() { + return this.#disabled; + } + public set disabled(value) { + this.#disabled = value; + + if (this._component) { + this._component.disabled = this.#disabled; + } + } + + #pathAddendum = new UmbRoutePathAddendumContext(this); + + #onSelected(event: UmbSelectedEvent) { + event.stopPropagation(); + 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; + if (!unique) throw new Error('No unique id found for item'); + this.dispatchEvent(new UmbDeselectedEvent(unique)); + } + + protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void { + super.firstUpdated(_changedProperties); + this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, 'entity-collection-item-card'); + } + + #createController(entityType: string) { + if (this.#extensionsController) { + this.#extensionsController.destroy(); + } + + this.#extensionsController = new UmbExtensionsElementInitializer( + this, + umbExtensionsRegistry, + 'entityCollectionItemCard', + (manifest: ManifestEntityCollectionItemCard) => manifest.forEntityTypes.includes(entityType), + (extensionControllers) => { + this._component?.remove(); + const component = + extensionControllers[0]?.component || document.createElement('umb-default-collection-item-card'); + + // 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.selectable = this.selectable; + component.selectOnly = this.selectOnly; + component.selected = this.selected; + component.disabled = this.disabled; + + component.addEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); + component.addEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); + + // Proxy the actions slot to the component + const slotElement = document.createElement('slot'); + slotElement.name = 'actions'; + slotElement.setAttribute('slot', 'actions'); + component.appendChild(slotElement); + + this._component = component; + }, + undefined, // We can leave the alias to undefined, as we destroy this our selfs. + undefined, + { single: true }, + ); + } + + override render() { + return html`${this._component}`; + } + + override destroy(): void { + this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); + this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); + super.destroy(); + } + + static override styles = [ + css` + :host { + display: block; + position: relative; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-entity-collection-item-card': UmbEntityCollectionItemCardElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts new file mode 100644 index 000000000000..7d2da5f2bfca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts @@ -0,0 +1,19 @@ +import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestEntityCollectionItemCard< + MetaType extends MetaEntityCollectionItemCard = MetaEntityCollectionItemCard, +> extends ManifestElement, + ManifestWithDynamicConditions { + type: 'entityCollectionItemCard'; + meta: MetaType; + forEntityTypes: Array; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface MetaEntityCollectionItemCard {} + +declare global { + interface UmbExtensionManifestMap { + umbManifestEntityCollectionItemCard: ManifestEntityCollectionItemCard; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/global-components.ts new file mode 100644 index 000000000000..522a7aad45d1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/global-components.ts @@ -0,0 +1 @@ +import './entity-collection-item-card.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/index.ts new file mode 100644 index 000000000000..4c51a29d874a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/index.ts @@ -0,0 +1 @@ +export * from './entity-collection-item-card.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/types.ts new file mode 100644 index 000000000000..2580ba8add53 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/types.ts @@ -0,0 +1 @@ +export type * from './entity-collection-item-card.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts new file mode 100644 index 000000000000..8a5109c75c8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts @@ -0,0 +1 @@ +import './entity-collection-item-card/global-components.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts index 528282ebdb29..ea7315fb154d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts @@ -1,4 +1,5 @@ import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +export type * from './entity-collection-item-card/types.js'; export interface UmbCollectionItemModel extends UmbEntityModel { unique: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/manifests.ts new file mode 100644 index 000000000000..94ec01feb046 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_USER_ENTITY_TYPE } from '../../entity.js'; + +export const manifests: Array = [ + { + type: 'entityCollectionItemCard', + alias: 'Umb.EntityCollectionItemCard.User', + name: 'User Entity Collection Item Card', + element: () => import('./user-collection-item-card.element.js'), + forEntityTypes: [UMB_USER_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/types.ts new file mode 100644 index 000000000000..8c1883ca79a1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/types.ts @@ -0,0 +1,3 @@ +import type { UmbUserDetailModel } from '../../types.js'; + +export type UmbUserCollectionItemModel = UmbUserDetailModel; From 852a9d74d772dee08b5b1352f727027d78640aaf Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 25 Nov 2025 11:21:10 +0100 Subject: [PATCH 02/18] implement user collection item card --- .../item/user-collection-item-card.element.ts | 160 ++++++++++++++++++ .../user/user/collection/manifests.ts | 3 + .../grid/user-grid-collection-view.element.ts | 103 +---------- 3 files changed, 167 insertions(+), 99 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts new file mode 100644 index 000000000000..b17e1a9aca0c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts @@ -0,0 +1,160 @@ +import { UMB_USER_WORKSPACE_PATH } from '../../paths.js'; +import { getDisplayStateFromUserStatus, TimeFormatOptions } from '../../utils.js'; +import { UmbUserKind } from '../../utils/user-kind.js'; +import type { UmbUserCollectionItemModel } from './types.js'; +import { + css, + customElement, + html, + ifDefined, + nothing, + property, + state, + when, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbUserGroupItemRepository, type UmbUserGroupItemModel } from '@umbraco-cms/backoffice/user-group'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api'; + +@customElement('umb-user-collection-item-card') +export class UmbUserCollectionItemCardElement extends UmbLitElement { + #item?: UmbUserCollectionItemModel | undefined; + + @property({ type: Object }) + public get item(): UmbUserCollectionItemModel | undefined { + return this.#item; + } + public set item(value: UmbUserCollectionItemModel | undefined) { + this.#item = value; + this.#loadUserGroups(); + } + + @property({ type: Boolean }) + selectable = false; + + @property({ type: Boolean }) + selected = false; + + @property({ type: Boolean }) + selectOnly = false; + + @state() + private _userGroupItems: Array = []; + + #userGroupItemRepository = new UmbUserGroupItemRepository(this); + + async #loadUserGroups() { + if (!this.item || this.item?.userGroupUniques.length === 0) { + this._userGroupItems = []; + return; + } + + const { data } = await this.#userGroupItemRepository.requestItems( + this.item.userGroupUniques.map((ref) => ref.unique), + ); + + this._userGroupItems = data ?? []; + } + + #onSelected(event: CustomEvent) { + if (!this.item) return; + event.stopPropagation(); + this.dispatchEvent(new UmbSelectedEvent(this.item.unique)); + } + + #onDeselected(event: CustomEvent) { + if (!this.item) return; + event.stopPropagation(); + this.dispatchEvent(new UmbDeselectedEvent(this.item.unique)); + } + + override render() { + if (!this.item) return nothing; + + return html` + + ${this.#renderUserTag()} ${this.#renderUserGroupNames()} ${this.#renderUserLoginDate()} + + + `; + } + + #renderUserTag() { + if (this.item?.state && this.item?.state === UserStateModel.ACTIVE) { + return nothing; + } + + const statusLook = this.item?.state ? getDisplayStateFromUserStatus(this.item.state) : undefined; + return html` + + + + `; + } + + #renderUserGroupNames() { + const userGroupNames = this._userGroupItems + .filter((userGroup) => + this.item?.userGroupUniques?.map((reference) => reference.unique).includes(userGroup.unique), + ) + .map((userGroup) => userGroup.name) + .join(', '); + + return html`
${userGroupNames}
`; + } + + #renderUserLoginDate() { + if (this.item?.kind === UmbUserKind.API) return nothing; + return html` + + `; + } + + static override styles = [ + css` + uui-card-user { + width: 100%; + justify-content: normal; + padding-top: var(--uui-size-space-5); + flex-direction: column; + + umb-user-avatar { + font-size: 1.6rem; + } + } + + .user-login-time { + margin-top: var(--uui-size-1); + } + `, + ]; +} + +export { UmbUserCollectionItemCardElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-collection-item-card': UmbUserCollectionItemCardElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts index 59374f44b52d..1a0c754f732c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts @@ -3,6 +3,8 @@ import { manifests as collectionActionManifests } from './action/manifests.js'; import { manifests as collectionMenuManifests } from './menu/manifests.js'; import { manifests as collectionRepositoryManifests } from './repository/manifests.js'; import { manifests as collectionViewManifests } from './views/manifests.js'; +import { manifests as itemManifests } from './item/manifests.js'; + import { UMB_USER_COLLECTION_ALIAS } from './constants.js'; export const manifests: Array = [ @@ -20,4 +22,5 @@ export const manifests: Array = [ ...collectionMenuManifests, ...collectionRepositoryManifests, ...collectionViewManifests, + ...itemManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/grid/user-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/grid/user-grid-collection-view.element.ts index 3f491f7f7460..778d2dc94846 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/grid/user-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/grid/user-grid-collection-view.element.ts @@ -1,24 +1,9 @@ -import { getDisplayStateFromUserStatus, TimeFormatOptions } from '../../../utils.js'; -import { UmbUserKind } from '../../../utils/index.js'; import { UMB_USER_COLLECTION_CONTEXT } from '../../user-collection.context-token.js'; -import { UMB_USER_WORKSPACE_PATH } from '../../../paths.js'; import type { UmbUserCollectionContext } from '../../user-collection.context.js'; import type { UmbUserDetailModel } from '../../../types.js'; -import { - css, - customElement, - html, - ifDefined, - nothing, - repeat, - state, - when, -} from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UmbUserGroupCollectionRepository } from '@umbraco-cms/backoffice/user-group'; -import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api'; -import type { UmbUserGroupDetailModel } from '@umbraco-cms/backoffice/user-group'; @customElement('umb-user-grid-collection-view') export class UmbUserGridCollectionViewElement extends UmbLitElement { @@ -31,13 +16,8 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement { @state() private _loading = false; - #userGroups: Array = []; - #collectionContext?: UmbUserCollectionContext; - // TODO: we need to use the item repository here - #userGroupCollectionRepository = new UmbUserGroupCollectionRepository(this); - constructor() { super(); @@ -56,18 +36,6 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement { 'umbCollectionItemsObserver', ); }); - - this.#requestUserGroups(); - } - - async #requestUserGroups() { - this._loading = true; - - const { data } = await this.#userGroupCollectionRepository.requestCollection(); - - this.#userGroups = data?.items ?? []; - - this._loading = false; } #onSelect(user: UmbUserDetailModel) { @@ -93,59 +61,13 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement { #renderUserCard(user: UmbUserDetailModel) { return html` - 0} ?selected=${this.#collectionContext?.selection.isSelected(user.unique)} @selected=${() => this.#onSelect(user)} - @deselected=${() => this.#onDeselect(user)}> - ${this.#renderUserTag(user)} ${this.#renderUserGroupNames(user)} ${this.#renderUserLoginDate(user)} - - - `; - } - - #renderUserTag(user: UmbUserDetailModel) { - if (user.state && user.state === UserStateModel.ACTIVE) { - return nothing; - } - - const statusLook = user.state ? getDisplayStateFromUserStatus(user.state) : undefined; - return html` - - - - `; - } - - #renderUserGroupNames(user: UmbUserDetailModel) { - const userGroupNames = this.#userGroups - .filter((userGroup) => user.userGroupUniques?.map((reference) => reference.unique).includes(userGroup.unique)) - .map((userGroup) => userGroup.name) - .join(', '); - - return html`
${userGroupNames}
`; - } - - #renderUserLoginDate(user: UmbUserDetailModel) { - if (user.kind === UmbUserKind.API) return nothing; - return html` - + @deselected=${() => this.#onDeselect(user)}> `; } @@ -162,27 +84,10 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--uui-size-space-4); } - - uui-card-user { - width: 100%; - justify-content: normal; - padding-top: var(--uui-size-space-5); - flex-direction: column; - - umb-user-avatar { - font-size: 1.6rem; - } - } - - .user-login-time { - margin-top: var(--uui-size-1); - } `, ]; } -export default UmbUserGridCollectionViewElement; - export { UmbUserGridCollectionViewElement as element }; declare global { From 0dc90ad5834847ea9db3eef81a4a766f4d87601a Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 25 Nov 2025 11:27:21 +0100 Subject: [PATCH 03/18] fix selection events --- .../default-collection-item-card.element.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts index 580a52ff54ea..e2021082172c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts @@ -1,6 +1,6 @@ import type { UmbCollectionItemModel } from '../types.js'; import { getItemFallbackName, getItemFallbackIcon } from '@umbraco-cms/backoffice/entity-item'; -import { UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import { customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -12,6 +12,12 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement { @property({ type: Boolean }) selectable = false; + @property({ type: Boolean }) + selected = false; + + @property({ type: Boolean }) + selectOnly = false; + #onSelected(event: CustomEvent) { if (!this.item) return; event.stopPropagation(); @@ -21,7 +27,7 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement { #onDeselected(event: CustomEvent) { if (!this.item) return; event.stopPropagation(); - this.dispatchEvent(new UmbSelectedEvent(this.item.unique)); + this.dispatchEvent(new UmbDeselectedEvent(this.item.unique)); } override render() { @@ -31,6 +37,8 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement { From 8c047e0ccfec1ddcfd77583edda2fdc2494ed345 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 25 Nov 2025 11:27:30 +0100 Subject: [PATCH 04/18] map to prop --- .../user/collection/item/user-collection-item-card.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts index b17e1a9aca0c..0f83827e13d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts @@ -76,7 +76,7 @@ export class UmbUserCollectionItemCardElement extends UmbLitElement { Date: Tue, 25 Nov 2025 12:56:30 +0100 Subject: [PATCH 05/18] add prop/attr for href --- .../default-collection-item-card.element.ts | 4 ++++ .../entity-collection-item-card.element.ts | 15 ++++++++++++++- .../item/user-collection-item-card.element.ts | 5 ++++- .../grid/user-grid-collection-view.element.ts | 2 ++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts index e2021082172c..4ac9d2a00e69 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts @@ -18,6 +18,9 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement { @property({ type: Boolean }) selectOnly = false; + @property({ type: String }) + href?: string; + #onSelected(event: CustomEvent) { if (!this.item) return; event.stopPropagation(); @@ -36,6 +39,7 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement { return html` = []; @@ -75,7 +78,7 @@ export class UmbUserCollectionItemCardElement extends UmbLitElement { return html` 0} ?selected=${this.#collectionContext?.selection.isSelected(user.unique)} From df1219a7211cb73fdf96580a81d30f69b941aab0 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 25 Nov 2025 15:27:24 +0100 Subject: [PATCH 06/18] add support for which detail properties to show --- .../entity-collection-item-card.element.ts | 14 ++++ .../packages/core/collection/item/types.ts | 6 ++ .../document-collection-item-card.element.ts | 79 +++++++++++++++++++ .../documents/collection/item/manifests.ts | 11 +++ .../documents/collection/item/types.ts | 27 +++++++ .../documents/collection/manifests.ts | 2 + .../documents/documents/collection/types.ts | 29 +------ .../document-grid-collection-view.element.ts | 13 +-- .../item/user-collection-item-card.element.ts | 1 - 9 files changed, 144 insertions(+), 38 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts index f39a2bdce031..eaa902ddec20 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts @@ -107,6 +107,19 @@ export class UmbEntityCollectionItemCardElement extends UmbLitElement { } } + #detailProperties?: Array; + @property({ type: Array, attribute: false }) + public get detailProperties() { + return this.#detailProperties; + } + public set detailProperties(value) { + this.#detailProperties = value; + + if (this._component) { + this._component.detailProperties = this.#detailProperties; + } + } + #pathAddendum = new UmbRoutePathAddendumContext(this); #onSelected(event: UmbSelectedEvent) { @@ -151,6 +164,7 @@ export class UmbEntityCollectionItemCardElement extends UmbLitElement { component.selected = this.selected; component.disabled = this.disabled; component.href = this.href; + component.detailProperties = this.detailProperties; component.addEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); component.addEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts index ea7315fb154d..d6bb4a00a44c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts @@ -6,3 +6,9 @@ export interface UmbCollectionItemModel extends UmbEntityModel { name?: string; icon?: string; } + +export interface UmbCollectionItemDetailPropertyConfig { + alias: string; + name: string; + isSystem: boolean; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts new file mode 100644 index 000000000000..efa1c9a09e18 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts @@ -0,0 +1,79 @@ +import type { UmbDocumentCollectionItemModel } from './types.js'; +import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import type { UmbCollectionItemDetailPropertyConfig } from '@umbraco-cms/backoffice/collection'; + +@customElement('umb-document-collection-item-card') +export class UmbDocumentCollectionItemCardElement extends UmbLitElement { + #item?: UmbDocumentCollectionItemModel | undefined; + + @property({ type: Object }) + public get item(): UmbDocumentCollectionItemModel | undefined { + return this.#item; + } + public set item(value: UmbDocumentCollectionItemModel | undefined) { + this.#item = value; + } + + @property({ type: Boolean }) + selectable = false; + + @property({ type: Boolean }) + selected = false; + + @property({ type: Boolean }) + selectOnly = false; + + @property({ type: String }) + href?: string; + + @property({ type: Array }) + detailProperties?: Array; + + #onSelected(event: CustomEvent) { + if (!this.item) return; + event.stopPropagation(); + this.dispatchEvent(new UmbSelectedEvent(this.item.unique)); + } + + #onDeselected(event: CustomEvent) { + if (!this.item) return; + event.stopPropagation(); + this.dispatchEvent(new UmbDeselectedEvent(this.item.unique)); + } + + override render() { + if (!this.item) return nothing; + return html` + + + + `; + } + + static override styles = [ + css` + umb-document-grid-collection-card { + width: 100%; + min-height: 180px; + } + `, + ]; +} + +export { UmbDocumentCollectionItemCardElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-collection-item-card': UmbDocumentCollectionItemCardElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/manifests.ts new file mode 100644 index 000000000000..461525e3a8ed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; + +export const manifests: Array = [ + { + type: 'entityCollectionItemCard', + alias: 'Umb.EntityCollectionItemCard.Document', + name: 'Document Entity Collection Item Card', + element: () => import('./document-collection-item-card.element.js'), + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/types.ts new file mode 100644 index 000000000000..3f2b97fe3324 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/types.ts @@ -0,0 +1,27 @@ +import type { UmbDocumentEntityType } from '../../entity.js'; +import type { UmbDocumentItemVariantModel } from '../../types.js'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbEntityWithFlags } from '@umbraco-cms/backoffice/entity-flag'; + +export interface UmbDocumentCollectionItemModel extends UmbEntityWithFlags { + ancestors: Array; + creator?: string | null; + documentType: { + unique: string; + icon: string; + alias: string; + }; + entityType: UmbDocumentEntityType; + isProtected: boolean; + isTrashed: boolean; + sortOrder: number; + unique: string; + updater?: string | null; + values: Array<{ alias: string; culture?: string; segment?: string; value: string }>; + variants: Array; +} + +export interface UmbEditableDocumentCollectionItemModel { + item: UmbDocumentCollectionItemModel; + editPath: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/manifests.ts index dbfdee4513da..bbb50b4b7815 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/manifests.ts @@ -1,6 +1,7 @@ import { manifests as collectionActionManifests } from './action/manifests.js'; import { manifests as collectionRepositoryManifests } from './repository/manifests.js'; import { manifests as collectionViewManifests } from './views/manifests.js'; +import { manifests as itemManifests } from './item/manifests.js'; import { UMB_DOCUMENT_COLLECTION_REPOSITORY_ALIAS, UMB_DOCUMENT_COLLECTION_ALIAS } from './constants.js'; export const manifests: Array = [ @@ -17,4 +18,5 @@ export const manifests: Array = [ ...collectionActionManifests, ...collectionRepositoryManifests, ...collectionViewManifests, + ...itemManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/types.ts index de40f79a2041..5a05cf264e67 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/types.ts @@ -1,9 +1,7 @@ -import type { UmbDocumentEntityType } from '../entity.js'; -import type { UmbDocumentItemVariantModel } from '../item/types.js'; -import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; -import type { UmbEntityWithFlags } from '@umbraco-cms/backoffice/entity-flag'; import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; +export type * from './item/types.js'; + export interface UmbDocumentCollectionFilterModel extends UmbCollectionFilterModel { unique: string; dataTypeId?: string; @@ -12,26 +10,3 @@ export interface UmbDocumentCollectionFilterModel extends UmbCollectionFilterMod orderDirection?: 'asc' | 'desc'; userDefinedProperties: Array<{ alias: string; header: string; isSystem: boolean }>; } - -export interface UmbDocumentCollectionItemModel extends UmbEntityWithFlags { - ancestors: Array; - creator?: string | null; - documentType: { - unique: string; - icon: string; - alias: string; - }; - entityType: UmbDocumentEntityType; - isProtected: boolean; - isTrashed: boolean; - sortOrder: number; - unique: string; - updater?: string | null; - values: Array<{ alias: string; culture?: string; segment?: string; value: string }>; - variants: Array; -} - -export interface UmbEditableDocumentCollectionItemModel { - item: UmbDocumentCollectionItemModel; - editPath: string; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts index 6f8fdb04fabb..78e4ddb40cd1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts @@ -84,18 +84,16 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement { #renderItem(item: UmbDocumentCollectionItemModel) { return html` - 0} ?selected=${this.#isSelected(item)} @selected=${() => this.#onSelect(item)} @deselected=${() => this.#onDeselect(item)}> - - + `; } @@ -112,11 +110,6 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--uui-size-space-4); } - - .document-grid-item { - width: 100%; - min-height: 180px; - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts index 825b725f6699..d7d9c3e4da22 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts @@ -1,4 +1,3 @@ -import { UMB_USER_WORKSPACE_PATH } from '../../paths.js'; import { getDisplayStateFromUserStatus, TimeFormatOptions } from '../../utils.js'; import { UmbUserKind } from '../../utils/user-kind.js'; import type { UmbUserCollectionItemModel } from './types.js'; From 0f59c3ec0d5fe84a75e0610520244adbdf17c134 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 07:26:30 +0100 Subject: [PATCH 07/18] update type import --- .../entity-collection-item-card.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts index eaa902ddec20..e821d6c3a22a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts @@ -1,4 +1,4 @@ -import type { UmbCollectionItemModel } from '../types.js'; +import type { UmbCollectionItemDetailPropertyConfig, UmbCollectionItemModel } from '../types.js'; import type { ManifestEntityCollectionItemCard } from './entity-collection-item-card.extension.js'; import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -107,7 +107,7 @@ export class UmbEntityCollectionItemCardElement extends UmbLitElement { } } - #detailProperties?: Array; + #detailProperties?: Array; @property({ type: Array, attribute: false }) public get detailProperties() { return this.#detailProperties; From 2aefe5efaafadf97ee1192df97974fd8314dba59 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 09:55:16 +0100 Subject: [PATCH 08/18] Update src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../entity-collection-item-card.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts index e821d6c3a22a..7cc47f260e0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts @@ -177,7 +177,7 @@ export class UmbEntityCollectionItemCardElement extends UmbLitElement { this._component = component; }, - undefined, // We can leave the alias to undefined, as we destroy this our selfs. + undefined, // We can leave the alias to undefined, as we destroy this ourselves. undefined, { single: true }, ); From 37b9df4f0e57a5de5c006c68c28294a73518ab05 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 10:03:35 +0100 Subject: [PATCH 09/18] import card in correct file --- .../collection/item/document-collection-item-card.element.ts | 2 ++ .../grid => item}/document-grid-collection-card.element.ts | 4 ++-- .../views/grid/document-grid-collection-view.element.ts | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) rename src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/{views/grid => item}/document-grid-collection-card.element.ts (96%) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts index efa1c9a09e18..77ed9935adee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts @@ -4,6 +4,8 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import type { UmbCollectionItemDetailPropertyConfig } from '@umbraco-cms/backoffice/collection'; +import './document-grid-collection-card.element.js'; + @customElement('umb-document-collection-item-card') export class UmbDocumentCollectionItemCardElement extends UmbLitElement { #item?: UmbDocumentCollectionItemModel | undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-grid-collection-card.element.ts similarity index 96% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-card.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-grid-collection-card.element.ts index 0178ecd6258c..ea4feb3b30df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-grid-collection-card.element.ts @@ -1,5 +1,5 @@ -import { UmbDocumentItemDataResolver } from '../../../item/document-item-data-resolver.js'; -import type { UmbDocumentCollectionItemModel } from '../../types.js'; +import { UmbDocumentItemDataResolver } from '../../item/document-item-data-resolver.js'; +import type { UmbDocumentCollectionItemModel } from '../types.js'; import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { fromCamelCase } from '@umbraco-cms/backoffice/utils'; import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts index 78e4ddb40cd1..3553604e645a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts @@ -7,7 +7,6 @@ import type { UmbDefaultCollectionContext, UmbCollectionColumnConfiguration } fr import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import '@umbraco-cms/backoffice/ufm'; -import './document-grid-collection-card.element.js'; @customElement('umb-document-grid-collection-view') export class UmbDocumentGridCollectionViewElement extends UmbLitElement { From 3dbe65abc04e0924f4e75f75174c2b263ef169d4 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 10:12:38 +0100 Subject: [PATCH 10/18] Fix event listener binding for selection events --- .../entity-collection-item-card.element.ts | 11 +++++++---- .../entity-item-ref/entity-item-ref.element.ts | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts index 7cc47f260e0b..f7e2774b6830 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts @@ -141,6 +141,9 @@ export class UmbEntityCollectionItemCardElement extends UmbLitElement { this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, 'entity-collection-item-card'); } + #boundOnSelected = this.#onSelected.bind(this); + #boundOnDeselected = this.#onDeselected.bind(this); + #createController(entityType: string) { if (this.#extensionsController) { this.#extensionsController.destroy(); @@ -166,8 +169,8 @@ export class UmbEntityCollectionItemCardElement extends UmbLitElement { component.href = this.href; component.detailProperties = this.detailProperties; - component.addEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); - component.addEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); + component.addEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); + component.addEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); // Proxy the actions slot to the component const slotElement = document.createElement('slot'); @@ -188,8 +191,8 @@ export class UmbEntityCollectionItemCardElement extends UmbLitElement { } override destroy(): void { - this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); - this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); + this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); + this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); super.destroy(); } 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 836007d29cd2..78a5b2597896 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 @@ -151,6 +151,9 @@ export class UmbEntityItemRefElement extends UmbLitElement { this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, 'entity-item-ref'); } + #boundOnSelected = this.#onSelected.bind(this); + #boundOnDeselected = this.#onDeselected.bind(this); + #createController(entityType: string) { if (this.#extensionsController) { this.#extensionsController.destroy(); @@ -175,8 +178,8 @@ export class UmbEntityItemRefElement extends UmbLitElement { component.selected = this.selected; component.disabled = this.disabled; - component.addEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); - component.addEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); + component.addEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); + component.addEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); // Proxy the actions slot to the component const slotElement = document.createElement('slot'); @@ -220,8 +223,8 @@ export class UmbEntityItemRefElement extends UmbLitElement { } override destroy(): void { - this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this)); - this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this)); + this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); + this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); super.destroy(); } From 88654035f9d417ef6e4ec3232b176616ee2115fe Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 10:34:33 +0100 Subject: [PATCH 11/18] implement disabled property for collection item cards --- .../default-collection-item-card.element.ts | 4 ++++ .../collection/item/document-collection-item-card.element.ts | 4 ++++ .../user/collection/item/user-collection-item-card.element.ts | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts index 4ac9d2a00e69..a0ae8323e4fc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts @@ -18,6 +18,9 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement { @property({ type: Boolean }) selectOnly = false; + @property({ type: Boolean }) + disabled = false; + @property({ type: String }) href?: string; @@ -43,6 +46,7 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement { ?selectable=${this.selectable} ?select-only=${this.selectOnly} ?selected=${this.selected} + ?disabled=${this.disabled} @selected=${this.#onSelected} @deselected=${this.#onDeselected}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts index 77ed9935adee..71c619c6f731 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts @@ -27,6 +27,9 @@ export class UmbDocumentCollectionItemCardElement extends UmbLitElement { @property({ type: Boolean }) selectOnly = false; + @property({ type: Boolean }) + disabled = false; + @property({ type: String }) href?: string; @@ -55,6 +58,7 @@ export class UmbDocumentCollectionItemCardElement extends UmbLitElement { ?selectable=${this.selectable} ?select-only=${this.selectOnly} ?selected=${this.selected} + ?disabled=${this.disabled} @selected=${this.#onSelected} @deselected=${this.#onDeselected}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts index d7d9c3e4da22..f5f52a147441 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts @@ -38,6 +38,9 @@ export class UmbUserCollectionItemCardElement extends UmbLitElement { @property({ type: Boolean }) selectOnly = false; + @property({ type: Boolean }) + disabled = false; + @property({ type: String }) href?: string; @@ -81,6 +84,7 @@ export class UmbUserCollectionItemCardElement extends UmbLitElement { ?selectable=${this.selectable} ?select-only=${this.selectOnly} ?selected=${this.selected} + ?disabled=${this.disabled} @selected=${this.#onSelected} @deselected=${this.#onDeselected}> ${this.#renderUserTag()} ${this.#renderUserGroupNames()} ${this.#renderUserLoginDate()} From 5c4b2b886413fe8327995a03a9ca3567e69224d1 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 13:33:06 +0100 Subject: [PATCH 12/18] init commit of collection item ref extension --- .../default-collection-item-ref.element.ts | 50 ++++ .../entity-collection-item-ref.element.ts | 213 ++++++++++++++++++ .../entity-collection-item-ref.extension.ts | 19 ++ .../global-components.ts | 1 + .../item/entity-collection-item-ref/index.ts | 1 + .../item/entity-collection-item-ref/types.ts | 1 + .../core/collection/item/global-components.ts | 1 + .../packages/core/collection/item/types.ts | 1 + 8 files changed, 287 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/global-components.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts new file mode 100644 index 000000000000..66a156e51ae9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts @@ -0,0 +1,50 @@ +import type { UmbCollectionItemModel } from '../types.js'; +import { getItemFallbackName, getItemFallbackIcon } from '@umbraco-cms/backoffice/entity-item'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import { customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-default-collection-item-ref') +export class UmbDefaultCollectionItemRefElement extends UmbLitElement { + @property({ type: Object }) + item?: UmbCollectionItemModel; + + @property({ type: Boolean }) + selectable = false; + + @property({ type: Boolean }) + selected = false; + + @property({ type: Boolean }) + selectOnly = false; + + @property({ type: Boolean }) + disabled = false; + + @property({ type: String }) + href?: string; + + #onSelected(event: CustomEvent) { + if (!this.item) return; + event.stopPropagation(); + this.dispatchEvent(new UmbSelectedEvent(this.item.unique)); + } + + #onDeselected(event: CustomEvent) { + if (!this.item) return; + event.stopPropagation(); + this.dispatchEvent(new UmbDeselectedEvent(this.item.unique)); + } + + override render() { + if (!this.item) return nothing; + + return html`
MY COLLECTION ITEM REF ELEMENT
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-default-collection-item-ref': UmbDefaultCollectionItemRefElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts new file mode 100644 index 000000000000..147db966470e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts @@ -0,0 +1,213 @@ +import type { UmbCollectionItemDetailPropertyConfig, UmbCollectionItemModel } from '../types.js'; +import type { ManifestEntityCollectionItemRef } from './entity-collection-item-ref.extension.js'; +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 { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; +import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; + +import './default-collection-item-ref.element.js'; + +@customElement('umb-entity-collection-item-ref') +export class UmbEntityCollectionItemRefElement extends UmbLitElement { + #extensionsController?: UmbExtensionsElementInitializer; + #item?: UmbCollectionItemModel; + + @state() + private _component?: any; // TODO: Add type + + @property({ type: Object, attribute: false }) + public set item(value: UmbCollectionItemModel | undefined) { + const oldValue = this.#item; + this.#item = value; + + if (value === oldValue) return; + if (!value) return; + + // If the component is already created and the entity type is the same, we can just update the item. + if (this._component && value.entityType === oldValue?.entityType) { + this._component.item = value; + return; + } + + this.#pathAddendum.setAddendum('collection-item-ref/' + value.entityType + '/' + value.unique); + + // 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(): UmbCollectionItemModel | undefined { + return this.#item; + } + + #selectable = false; + @property({ type: Boolean, reflect: true }) + public get selectable() { + return this.#selectable; + } + public set selectable(value) { + this.#selectable = value; + + if (this._component) { + this._component.selectable = this.#selectable; + } + } + + #selectOnly = false; + @property({ type: Boolean, attribute: 'select-only', reflect: true }) + public get selectOnly() { + return this.#selectOnly; + } + public set selectOnly(value) { + this.#selectOnly = value; + + if (this._component) { + this._component.selectOnly = this.#selectOnly; + } + } + + #selected = false; + @property({ type: Boolean, reflect: true }) + public get selected() { + return this.#selected; + } + public set selected(value) { + this.#selected = value; + + if (this._component) { + this._component.selected = this.#selected; + } + } + + #disabled = false; + @property({ type: Boolean, reflect: true }) + public get disabled() { + return this.#disabled; + } + public set disabled(value) { + this.#disabled = value; + + if (this._component) { + this._component.disabled = this.#disabled; + } + } + + #href?: string; + @property({ type: String, reflect: true }) + public get href() { + return this.#href; + } + public set href(value) { + this.#href = value; + + if (this._component) { + this._component.href = this.#href; + } + } + + #detailProperties?: Array; + @property({ type: Array, attribute: false }) + public get detailProperties() { + return this.#detailProperties; + } + public set detailProperties(value) { + this.#detailProperties = value; + + if (this._component) { + this._component.detailProperties = this.#detailProperties; + } + } + + #pathAddendum = new UmbRoutePathAddendumContext(this); + + #onSelected(event: UmbSelectedEvent) { + event.stopPropagation(); + 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; + if (!unique) throw new Error('No unique id found for item'); + this.dispatchEvent(new UmbDeselectedEvent(unique)); + } + + protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void { + super.firstUpdated(_changedProperties); + this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, 'entity-collection-item-ref'); + } + + #boundOnSelected = this.#onSelected.bind(this); + #boundOnDeselected = this.#onDeselected.bind(this); + + #createController(entityType: string) { + if (this.#extensionsController) { + this.#extensionsController.destroy(); + } + + this.#extensionsController = new UmbExtensionsElementInitializer( + this, + umbExtensionsRegistry, + 'entityCollectionItemRef', + (manifest: ManifestEntityCollectionItemRef) => manifest.forEntityTypes.includes(entityType), + (extensionControllers) => { + this._component?.remove(); + const component = + extensionControllers[0]?.component || document.createElement('umb-default-collection-item-ref'); + + // 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.selectable = this.selectable; + component.selectOnly = this.selectOnly; + component.selected = this.selected; + component.disabled = this.disabled; + component.href = this.href; + component.detailProperties = this.detailProperties; + + component.addEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); + component.addEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); + + // Proxy the actions slot to the component + const slotElement = document.createElement('slot'); + slotElement.name = 'actions'; + slotElement.setAttribute('slot', 'actions'); + component.appendChild(slotElement); + + this._component = component; + }, + undefined, // We can leave the alias to undefined, as we destroy this ourselves. + undefined, + { single: true }, + ); + } + + override render() { + return html`${this._component}`; + } + + override destroy(): void { + this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); + this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); + super.destroy(); + } + + static override styles = [ + css` + :host { + display: block; + position: relative; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-entity-collection-item-ref': UmbEntityCollectionItemRefElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts new file mode 100644 index 000000000000..787cdeaf4c33 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts @@ -0,0 +1,19 @@ +import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestEntityCollectionItemRef< + MetaType extends MetaEntityCollectionItemRef = MetaEntityCollectionItemRef, +> extends ManifestElement, + ManifestWithDynamicConditions { + type: 'entityCollectionItemRef'; + meta: MetaType; + forEntityTypes: Array; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface MetaEntityCollectionItemRef {} + +declare global { + interface UmbExtensionManifestMap { + umbManifestEntityCollectionItemRef: ManifestEntityCollectionItemRef; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/global-components.ts new file mode 100644 index 000000000000..522a7aad45d1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/global-components.ts @@ -0,0 +1 @@ +import './entity-collection-item-card.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/index.ts new file mode 100644 index 000000000000..057c629abb5e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/index.ts @@ -0,0 +1 @@ +export * from './entity-collection-item-ref.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/types.ts new file mode 100644 index 000000000000..cd88329aeecc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/types.ts @@ -0,0 +1 @@ +export type * from './entity-collection-item-ref.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts index 8a5109c75c8f..9cde32e01745 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts @@ -1 +1,2 @@ import './entity-collection-item-card/global-components.js'; +import './entity-collection-item-ref.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts index d6bb4a00a44c..a263bd192095 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts @@ -1,5 +1,6 @@ import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; export type * from './entity-collection-item-card/types.js'; +export type * from './entity-collection-item-ref/types.js'; export interface UmbCollectionItemModel extends UmbEntityModel { unique: string; From 4450ae9aa3e0d7c6b62ecac35e54c93e247d6ecc Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 13:36:49 +0100 Subject: [PATCH 13/18] fix imports --- .../item/entity-collection-item-ref/global-components.ts | 2 +- .../src/packages/core/collection/item/global-components.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/global-components.ts index 522a7aad45d1..4636687bd2ab 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/global-components.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/global-components.ts @@ -1 +1 @@ -import './entity-collection-item-card.element.js'; +import './entity-collection-item-ref.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts index 9cde32e01745..7e1a04b92b0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/global-components.ts @@ -1,2 +1,2 @@ import './entity-collection-item-card/global-components.js'; -import './entity-collection-item-ref.element.js'; +import './entity-collection-item-ref/global-components.js'; From e98822eaa4a056d3738132305e04650c066ebf0b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 13:52:10 +0100 Subject: [PATCH 14/18] add element interface --- .../default-collection-item-card.element.ts | 3 ++- .../entity-collection-item-card.extension.ts | 3 ++- ...ntity-collection-item-element.interface.ts | 24 +++++++++++++++++++ .../default-collection-item-ref.element.ts | 3 ++- .../entity-collection-item-ref.extension.ts | 3 ++- 5 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-element.interface.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts index a0ae8323e4fc..8094b88486af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/default-collection-item-card.element.ts @@ -1,11 +1,12 @@ import type { UmbCollectionItemModel } from '../types.js'; +import type { UmbEntityCollectionItemElement } from '../entity-collection-item-element.interface.js'; import { getItemFallbackName, getItemFallbackIcon } from '@umbraco-cms/backoffice/entity-item'; import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import { customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-default-collection-item-card') -export class UmbDefaultCollectionItemCardElement extends UmbLitElement { +export class UmbDefaultCollectionItemCardElement extends UmbLitElement implements UmbEntityCollectionItemElement { @property({ type: Object }) item?: UmbCollectionItemModel; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts index 7d2da5f2bfca..a85d83b36ac0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts @@ -1,8 +1,9 @@ +import type { UmbEntityCollectionItemElement } from '../entity-collection-item-element.interface.js'; import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; export interface ManifestEntityCollectionItemCard< MetaType extends MetaEntityCollectionItemCard = MetaEntityCollectionItemCard, -> extends ManifestElement, +> extends ManifestElement, ManifestWithDynamicConditions { type: 'entityCollectionItemCard'; meta: MetaType; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-element.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-element.interface.ts new file mode 100644 index 000000000000..0b6f076383d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-element.interface.ts @@ -0,0 +1,24 @@ +import type { UmbCollectionItemModel } from './types.js'; + +/** + * An interface for elements that render collection items representing entities. + */ +export interface UmbEntityCollectionItemElement extends HTMLElement { + /** The collection item model to render. */ + item?: UmbCollectionItemModel | undefined; + + /** Whether the item should render with selection affordances. */ + selectable?: boolean; + + /** When true, the item only supports selection (no navigation). */ + selectOnly?: boolean; + + /** Whether the item is currently selected. */ + selected?: boolean; + + /** Whether the item is disabled. */ + disabled?: boolean; + + /** Optional href used by card/ref renderers to provide a link. */ + href?: string | undefined; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts index 66a156e51ae9..20cc4f34cfa9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts @@ -1,11 +1,12 @@ import type { UmbCollectionItemModel } from '../types.js'; +import type { UmbEntityCollectionItemElement } from '../entity-collection-item-element.interface.js'; import { getItemFallbackName, getItemFallbackIcon } from '@umbraco-cms/backoffice/entity-item'; import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import { customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-default-collection-item-ref') -export class UmbDefaultCollectionItemRefElement extends UmbLitElement { +export class UmbDefaultCollectionItemRefElement extends UmbLitElement implements UmbEntityCollectionItemElement { @property({ type: Object }) item?: UmbCollectionItemModel; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts index 787cdeaf4c33..30d84727a4ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts @@ -1,8 +1,9 @@ +import type { UmbEntityCollectionItemElement } from '../entity-collection-item-element.interface.js'; import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; export interface ManifestEntityCollectionItemRef< MetaType extends MetaEntityCollectionItemRef = MetaEntityCollectionItemRef, -> extends ManifestElement, +> extends ManifestElement, ManifestWithDynamicConditions { type: 'entityCollectionItemRef'; meta: MetaType; From c29817b0691d4494caf58fb4891cec6af534bb3e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 Nov 2025 13:54:48 +0100 Subject: [PATCH 15/18] Implement UmbEntityCollectionItemElement interface in item cards Added the UmbEntityCollectionItemElement interface to document and user collection item card elements for improved type safety and consistency. Updated type exports to include the new interface. --- .../src/packages/core/collection/item/types.ts | 1 + .../item/document-collection-item-card.element.ts | 7 +++++-- .../collection/item/user-collection-item-card.element.ts | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts index a263bd192095..e96639948a0a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts @@ -1,6 +1,7 @@ import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; export type * from './entity-collection-item-card/types.js'; export type * from './entity-collection-item-ref/types.js'; +export type * from './entity-collection-item-element.interface.js'; export interface UmbCollectionItemModel extends UmbEntityModel { unique: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts index 71c619c6f731..1943c679b639 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts @@ -2,12 +2,15 @@ import type { UmbDocumentCollectionItemModel } from './types.js'; import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; -import type { UmbCollectionItemDetailPropertyConfig } from '@umbraco-cms/backoffice/collection'; +import type { + UmbCollectionItemDetailPropertyConfig, + UmbEntityCollectionItemElement, +} from '@umbraco-cms/backoffice/collection'; import './document-grid-collection-card.element.js'; @customElement('umb-document-collection-item-card') -export class UmbDocumentCollectionItemCardElement extends UmbLitElement { +export class UmbDocumentCollectionItemCardElement extends UmbLitElement implements UmbEntityCollectionItemElement { #item?: UmbDocumentCollectionItemModel | undefined; @property({ type: Object }) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts index f5f52a147441..a6890a3a03c9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts @@ -15,9 +15,10 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbUserGroupItemRepository, type UmbUserGroupItemModel } from '@umbraco-cms/backoffice/user-group'; import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbEntityCollectionItemElement } from '@umbraco-cms/backoffice/collection'; @customElement('umb-user-collection-item-card') -export class UmbUserCollectionItemCardElement extends UmbLitElement { +export class UmbUserCollectionItemCardElement extends UmbLitElement implements UmbEntityCollectionItemElement { #item?: UmbUserCollectionItemModel | undefined; @property({ type: Object }) From 801f4d6db1e053ca464b85cee07f8b7a391935ad Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 28 Nov 2025 11:32:26 +0100 Subject: [PATCH 16/18] Update collection item ref to use uui-ref-node Replaces the placeholder div with a uui-ref-node component, passing relevant item properties and event handlers. Adds dynamic icon rendering using umb-icon. --- .../default-collection-item-ref.element.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts index 20cc4f34cfa9..58646df21de5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/default-collection-item-ref.element.ts @@ -40,7 +40,23 @@ export class UmbDefaultCollectionItemRefElement extends UmbLitElement implements override render() { if (!this.item) return nothing; - return html`
MY COLLECTION ITEM REF ELEMENT
`; + return html` + + ${this.#renderIcon(this.item)} + `; + } + + #renderIcon(item: UmbCollectionItemModel) { + const icon = item.icon || getItemFallbackIcon(); + return html``; } } From d6b1caa710133bc1dabf77a3635d7c35e945a87f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 28 Nov 2025 13:12:57 +0100 Subject: [PATCH 17/18] Refactor entity collection item elements to use shared base Introduces a new abstract base class for entity collection item elements, consolidating shared logic for card and ref variants. Updates card and ref element implementations to extend the new base, and refactors extension manifest interfaces for consistency. This improves maintainability and reduces code duplication. --- .../entity-collection-item-card.element.ts | 193 +---------------- .../entity-collection-item-card.extension.ts | 8 +- .../entity-collection-item-ref.element.ts | 193 +---------------- .../entity-collection-item-ref.extension.ts | 8 +- .../item/entity-collection-item.extension.ts | 20 ++ ...ty-collection-item-element-base.element.ts | 198 ++++++++++++++++++ 6 files changed, 244 insertions(+), 376 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item.extension.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/item/umb-entity-collection-item-element-base.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts index f7e2774b6830..853331c24b0e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts @@ -1,201 +1,30 @@ -import type { UmbCollectionItemDetailPropertyConfig, UmbCollectionItemModel } from '../types.js'; -import type { ManifestEntityCollectionItemCard } from './entity-collection-item-card.extension.js'; -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 { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; -import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; -import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { UmbEntityCollectionItemElementBase } from '../umb-entity-collection-item-element-base.element.js'; +import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; import './default-collection-item-card.element.js'; @customElement('umb-entity-collection-item-card') -export class UmbEntityCollectionItemCardElement extends UmbLitElement { - #extensionsController?: UmbExtensionsElementInitializer; - #item?: UmbCollectionItemModel; - - @state() - private _component?: any; // TODO: Add type - - @property({ type: Object, attribute: false }) - public set item(value: UmbCollectionItemModel | undefined) { - const oldValue = this.#item; - this.#item = value; - - if (value === oldValue) return; - if (!value) return; - - // If the component is already created and the entity type is the same, we can just update the item. - if (this._component && value.entityType === oldValue?.entityType) { - this._component.item = value; - return; - } - - this.#pathAddendum.setAddendum('collection-item-card/' + value.entityType + '/' + value.unique); - - // 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(): UmbCollectionItemModel | undefined { - return this.#item; - } - - #selectable = false; - @property({ type: Boolean, reflect: true }) - public get selectable() { - return this.#selectable; - } - public set selectable(value) { - this.#selectable = value; - - if (this._component) { - this._component.selectable = this.#selectable; - } - } - - #selectOnly = false; - @property({ type: Boolean, attribute: 'select-only', reflect: true }) - public get selectOnly() { - return this.#selectOnly; - } - public set selectOnly(value) { - this.#selectOnly = value; - - if (this._component) { - this._component.selectOnly = this.#selectOnly; - } - } - - #selected = false; - @property({ type: Boolean, reflect: true }) - public get selected() { - return this.#selected; - } - public set selected(value) { - this.#selected = value; - - if (this._component) { - this._component.selected = this.#selected; - } - } - - #disabled = false; - @property({ type: Boolean, reflect: true }) - public get disabled() { - return this.#disabled; +export class UmbEntityCollectionItemCardElement extends UmbEntityCollectionItemElementBase { + protected getExtensionType(): string { + return 'entityCollectionItemCard'; } - public set disabled(value) { - this.#disabled = value; - if (this._component) { - this._component.disabled = this.#disabled; - } + protected createFallbackElement(): HTMLElement { + return document.createElement('umb-default-collection-item-card'); } - #href?: string; - @property({ type: String, reflect: true }) - public get href() { - return this.#href; + protected getPathAddendum(entityType: string, unique: string): string { + return 'collection-item-card/' + entityType + '/' + unique; } - public set href(value) { - this.#href = value; - if (this._component) { - this._component.href = this.#href; - } - } - - #detailProperties?: Array; - @property({ type: Array, attribute: false }) - public get detailProperties() { - return this.#detailProperties; - } - public set detailProperties(value) { - this.#detailProperties = value; - - if (this._component) { - this._component.detailProperties = this.#detailProperties; - } - } - - #pathAddendum = new UmbRoutePathAddendumContext(this); - - #onSelected(event: UmbSelectedEvent) { - event.stopPropagation(); - 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; - if (!unique) throw new Error('No unique id found for item'); - this.dispatchEvent(new UmbDeselectedEvent(unique)); - } - - protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void { - super.firstUpdated(_changedProperties); - this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, 'entity-collection-item-card'); - } - - #boundOnSelected = this.#onSelected.bind(this); - #boundOnDeselected = this.#onDeselected.bind(this); - - #createController(entityType: string) { - if (this.#extensionsController) { - this.#extensionsController.destroy(); - } - - this.#extensionsController = new UmbExtensionsElementInitializer( - this, - umbExtensionsRegistry, - 'entityCollectionItemCard', - (manifest: ManifestEntityCollectionItemCard) => manifest.forEntityTypes.includes(entityType), - (extensionControllers) => { - this._component?.remove(); - const component = - extensionControllers[0]?.component || document.createElement('umb-default-collection-item-card'); - - // 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.selectable = this.selectable; - component.selectOnly = this.selectOnly; - component.selected = this.selected; - component.disabled = this.disabled; - component.href = this.href; - component.detailProperties = this.detailProperties; - - component.addEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); - component.addEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); - - // Proxy the actions slot to the component - const slotElement = document.createElement('slot'); - slotElement.name = 'actions'; - slotElement.setAttribute('slot', 'actions'); - component.appendChild(slotElement); - - this._component = component; - }, - undefined, // We can leave the alias to undefined, as we destroy this ourselves. - undefined, - { single: true }, - ); + protected getMarkAttributeName(): string { + return 'entity-collection-item-card'; } override render() { return html`${this._component}`; } - override destroy(): void { - this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); - this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); - super.destroy(); - } - static override styles = [ css` :host { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts index a85d83b36ac0..a72348ec1d34 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.extension.ts @@ -1,13 +1,9 @@ -import type { UmbEntityCollectionItemElement } from '../entity-collection-item-element.interface.js'; -import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; +import type { ManifestEntityCollectionItemBase } from '../entity-collection-item.extension.js'; export interface ManifestEntityCollectionItemCard< MetaType extends MetaEntityCollectionItemCard = MetaEntityCollectionItemCard, -> extends ManifestElement, - ManifestWithDynamicConditions { +> extends ManifestEntityCollectionItemBase { type: 'entityCollectionItemCard'; - meta: MetaType; - forEntityTypes: Array; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts index 147db966470e..5d866d827384 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts @@ -1,201 +1,30 @@ -import type { UmbCollectionItemDetailPropertyConfig, UmbCollectionItemModel } from '../types.js'; -import type { ManifestEntityCollectionItemRef } from './entity-collection-item-ref.extension.js'; -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 { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; -import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; -import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { UmbEntityCollectionItemElementBase } from '../umb-entity-collection-item-element-base.element.js'; +import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; import './default-collection-item-ref.element.js'; @customElement('umb-entity-collection-item-ref') -export class UmbEntityCollectionItemRefElement extends UmbLitElement { - #extensionsController?: UmbExtensionsElementInitializer; - #item?: UmbCollectionItemModel; - - @state() - private _component?: any; // TODO: Add type - - @property({ type: Object, attribute: false }) - public set item(value: UmbCollectionItemModel | undefined) { - const oldValue = this.#item; - this.#item = value; - - if (value === oldValue) return; - if (!value) return; - - // If the component is already created and the entity type is the same, we can just update the item. - if (this._component && value.entityType === oldValue?.entityType) { - this._component.item = value; - return; - } - - this.#pathAddendum.setAddendum('collection-item-ref/' + value.entityType + '/' + value.unique); - - // 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(): UmbCollectionItemModel | undefined { - return this.#item; - } - - #selectable = false; - @property({ type: Boolean, reflect: true }) - public get selectable() { - return this.#selectable; - } - public set selectable(value) { - this.#selectable = value; - - if (this._component) { - this._component.selectable = this.#selectable; - } - } - - #selectOnly = false; - @property({ type: Boolean, attribute: 'select-only', reflect: true }) - public get selectOnly() { - return this.#selectOnly; - } - public set selectOnly(value) { - this.#selectOnly = value; - - if (this._component) { - this._component.selectOnly = this.#selectOnly; - } - } - - #selected = false; - @property({ type: Boolean, reflect: true }) - public get selected() { - return this.#selected; - } - public set selected(value) { - this.#selected = value; - - if (this._component) { - this._component.selected = this.#selected; - } - } - - #disabled = false; - @property({ type: Boolean, reflect: true }) - public get disabled() { - return this.#disabled; +export class UmbEntityCollectionItemRefElement extends UmbEntityCollectionItemElementBase { + protected getExtensionType(): string { + return 'entityCollectionItemRef'; } - public set disabled(value) { - this.#disabled = value; - if (this._component) { - this._component.disabled = this.#disabled; - } + protected createFallbackElement(): HTMLElement { + return document.createElement('umb-default-collection-item-ref'); } - #href?: string; - @property({ type: String, reflect: true }) - public get href() { - return this.#href; + protected getPathAddendum(entityType: string, unique: string): string { + return 'collection-item-ref/' + entityType + '/' + unique; } - public set href(value) { - this.#href = value; - if (this._component) { - this._component.href = this.#href; - } - } - - #detailProperties?: Array; - @property({ type: Array, attribute: false }) - public get detailProperties() { - return this.#detailProperties; - } - public set detailProperties(value) { - this.#detailProperties = value; - - if (this._component) { - this._component.detailProperties = this.#detailProperties; - } - } - - #pathAddendum = new UmbRoutePathAddendumContext(this); - - #onSelected(event: UmbSelectedEvent) { - event.stopPropagation(); - 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; - if (!unique) throw new Error('No unique id found for item'); - this.dispatchEvent(new UmbDeselectedEvent(unique)); - } - - protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void { - super.firstUpdated(_changedProperties); - this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, 'entity-collection-item-ref'); - } - - #boundOnSelected = this.#onSelected.bind(this); - #boundOnDeselected = this.#onDeselected.bind(this); - - #createController(entityType: string) { - if (this.#extensionsController) { - this.#extensionsController.destroy(); - } - - this.#extensionsController = new UmbExtensionsElementInitializer( - this, - umbExtensionsRegistry, - 'entityCollectionItemRef', - (manifest: ManifestEntityCollectionItemRef) => manifest.forEntityTypes.includes(entityType), - (extensionControllers) => { - this._component?.remove(); - const component = - extensionControllers[0]?.component || document.createElement('umb-default-collection-item-ref'); - - // 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.selectable = this.selectable; - component.selectOnly = this.selectOnly; - component.selected = this.selected; - component.disabled = this.disabled; - component.href = this.href; - component.detailProperties = this.detailProperties; - - component.addEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); - component.addEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); - - // Proxy the actions slot to the component - const slotElement = document.createElement('slot'); - slotElement.name = 'actions'; - slotElement.setAttribute('slot', 'actions'); - component.appendChild(slotElement); - - this._component = component; - }, - undefined, // We can leave the alias to undefined, as we destroy this ourselves. - undefined, - { single: true }, - ); + protected getMarkAttributeName(): string { + return 'entity-collection-item-ref'; } override render() { return html`${this._component}`; } - override destroy(): void { - this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); - this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); - super.destroy(); - } - static override styles = [ css` :host { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts index 30d84727a4ea..0d3234176171 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.extension.ts @@ -1,13 +1,9 @@ -import type { UmbEntityCollectionItemElement } from '../entity-collection-item-element.interface.js'; -import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; +import type { ManifestEntityCollectionItemBase } from '../entity-collection-item.extension.js'; export interface ManifestEntityCollectionItemRef< MetaType extends MetaEntityCollectionItemRef = MetaEntityCollectionItemRef, -> extends ManifestElement, - ManifestWithDynamicConditions { +> extends ManifestEntityCollectionItemBase { type: 'entityCollectionItemRef'; - meta: MetaType; - forEntityTypes: Array; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item.extension.ts new file mode 100644 index 000000000000..c91872743d02 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item.extension.ts @@ -0,0 +1,20 @@ +import type { UmbEntityCollectionItemElement } from './entity-collection-item-element.interface.js'; +import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; + +/** + * Base interface for entity collection item manifests. + * Shared by card and ref variants. + */ +export interface ManifestEntityCollectionItemBase + extends ManifestElement, + ManifestWithDynamicConditions { + /** + * The entity types this collection item supports. + */ + forEntityTypes: Array; + + /** + * Additional metadata for the collection item. + */ + meta: MetaType; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/umb-entity-collection-item-element-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/umb-entity-collection-item-element-base.element.ts new file mode 100644 index 000000000000..a021e2304427 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/umb-entity-collection-item-element-base.element.ts @@ -0,0 +1,198 @@ +import type { UmbCollectionItemDetailPropertyConfig, UmbCollectionItemModel } from './types.js'; +import type { ManifestEntityCollectionItemBase } from './entity-collection-item.extension.js'; +import { 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 { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; +import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; + +export abstract class UmbEntityCollectionItemElementBase extends UmbLitElement { + #extensionsController?: UmbExtensionsElementInitializer; + #item?: UmbCollectionItemModel; + + @state() + protected _component?: any; // TODO: Add type + + @property({ type: Object, attribute: false }) + public set item(value: UmbCollectionItemModel | undefined) { + const oldValue = this.#item; + this.#item = value; + + if (value === oldValue) return; + if (!value) return; + + // If the component is already created and the entity type is the same, we can just update the item. + if (this._component && value.entityType === oldValue?.entityType) { + this._component.item = value; + return; + } + + this.#pathAddendum.setAddendum(this.getPathAddendum(value.entityType, value.unique)); + + // 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(): UmbCollectionItemModel | undefined { + return this.#item; + } + + #selectable = false; + @property({ type: Boolean, reflect: true }) + public get selectable() { + return this.#selectable; + } + public set selectable(value) { + this.#selectable = value; + + if (this._component) { + this._component.selectable = this.#selectable; + } + } + + #selectOnly = false; + @property({ type: Boolean, attribute: 'select-only', reflect: true }) + public get selectOnly() { + return this.#selectOnly; + } + public set selectOnly(value) { + this.#selectOnly = value; + + if (this._component) { + this._component.selectOnly = this.#selectOnly; + } + } + + #selected = false; + @property({ type: Boolean, reflect: true }) + public get selected() { + return this.#selected; + } + public set selected(value) { + this.#selected = value; + + if (this._component) { + this._component.selected = this.#selected; + } + } + + #disabled = false; + @property({ type: Boolean, reflect: true }) + public get disabled() { + return this.#disabled; + } + public set disabled(value) { + this.#disabled = value; + + if (this._component) { + this._component.disabled = this.#disabled; + } + } + + #href?: string; + @property({ type: String, reflect: true }) + public get href() { + return this.#href; + } + public set href(value) { + this.#href = value; + + if (this._component) { + this._component.href = this.#href; + } + } + + #detailProperties?: Array; + @property({ type: Array, attribute: false }) + public get detailProperties() { + return this.#detailProperties; + } + public set detailProperties(value) { + this.#detailProperties = value; + + if (this._component) { + this._component.detailProperties = this.#detailProperties; + } + } + + #pathAddendum = new UmbRoutePathAddendumContext(this); + + #onSelected(event: UmbSelectedEvent) { + event.stopPropagation(); + 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; + if (!unique) throw new Error('No unique id found for item'); + this.dispatchEvent(new UmbDeselectedEvent(unique)); + } + + protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void { + super.firstUpdated(_changedProperties); + this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, this.getMarkAttributeName()); + } + + #boundOnSelected = this.#onSelected.bind(this); + #boundOnDeselected = this.#onDeselected.bind(this); + + #createController(entityType: string) { + if (this.#extensionsController) { + this.#extensionsController.destroy(); + } + + this.#extensionsController = new UmbExtensionsElementInitializer( + this, + umbExtensionsRegistry, + this.getExtensionType(), + (manifest: ManifestEntityCollectionItemBase) => manifest.forEntityTypes.includes(entityType), + (extensionControllers) => { + this._component?.remove(); + const component = extensionControllers[0]?.component || this.createFallbackElement(); + + // 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.selectable = this.selectable; + component.selectOnly = this.selectOnly; + component.selected = this.selected; + component.disabled = this.disabled; + component.href = this.href; + component.detailProperties = this.detailProperties; + + component.addEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); + component.addEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); + + // Proxy the actions slot to the component + const slotElement = document.createElement('slot'); + slotElement.name = 'actions'; + slotElement.setAttribute('slot', 'actions'); + component.appendChild(slotElement); + + this._component = component; + }, + undefined, // We can leave the alias to undefined, as we destroy this ourselves. + undefined, + { single: true }, + ); + } + + override destroy(): void { + this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#boundOnSelected); + this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#boundOnDeselected); + super.destroy(); + } + + /** + * Abstract methods that subclasses must implement + */ + protected abstract getExtensionType(): string; + protected abstract createFallbackElement(): HTMLElement; + protected abstract getPathAddendum(entityType: string, unique: string): string; + protected abstract getMarkAttributeName(): string; +} From 3e1063ca28031cca839f207bb2805b4a6c6a3331 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 28 Nov 2025 13:15:53 +0100 Subject: [PATCH 18/18] use class instead of magic string --- .../entity-collection-item-card.element.ts | 5 ++--- .../entity-collection-item-ref.element.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts index 853331c24b0e..45cf6cb55c70 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-card/entity-collection-item-card.element.ts @@ -1,8 +1,7 @@ import { UmbEntityCollectionItemElementBase } from '../umb-entity-collection-item-element-base.element.js'; +import { UmbDefaultCollectionItemCardElement } from './default-collection-item-card.element.js'; import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; -import './default-collection-item-card.element.js'; - @customElement('umb-entity-collection-item-card') export class UmbEntityCollectionItemCardElement extends UmbEntityCollectionItemElementBase { protected getExtensionType(): string { @@ -10,7 +9,7 @@ export class UmbEntityCollectionItemCardElement extends UmbEntityCollectionItemE } protected createFallbackElement(): HTMLElement { - return document.createElement('umb-default-collection-item-card'); + return new UmbDefaultCollectionItemCardElement(); } protected getPathAddendum(entityType: string, unique: string): string { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts index 5d866d827384..316dc3d24716 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/entity-collection-item-ref/entity-collection-item-ref.element.ts @@ -1,8 +1,7 @@ import { UmbEntityCollectionItemElementBase } from '../umb-entity-collection-item-element-base.element.js'; +import { UmbDefaultCollectionItemRefElement } from './default-collection-item-ref.element.js'; import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; -import './default-collection-item-ref.element.js'; - @customElement('umb-entity-collection-item-ref') export class UmbEntityCollectionItemRefElement extends UmbEntityCollectionItemElementBase { protected getExtensionType(): string { @@ -10,7 +9,7 @@ export class UmbEntityCollectionItemRefElement extends UmbEntityCollectionItemEl } protected createFallbackElement(): HTMLElement { - return document.createElement('umb-default-collection-item-ref'); + return new UmbDefaultCollectionItemRefElement(); } protected getPathAddendum(entityType: string, unique: string): string {