From cc1c99bf62f63b187eb9902a13174a7df07c8f53 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 25 Nov 2025 10:10:09 +0100 Subject: [PATCH 01/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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 { From 4d6c622d672ed000a2402e85e76cffb694ff75f0 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 1 Dec 2025 12:47:27 +0100 Subject: [PATCH 19/48] introduce ref and card collection view kinds --- .../default/collection-default.context.ts | 4 +- .../core/collection/extensions/types.ts | 1 - .../src/packages/core/collection/manifests.ts | 2 + .../src/packages/core/collection/types.ts | 3 +- .../view/card/card-collection-view.element.ts | 102 ++++++++++++++++++ .../core/collection/view/card/manifests.ts | 21 ++++ .../collection-view.extension.ts | 0 .../collection-view.manager.test.ts | 4 +- .../{ => view}/collection-view.manager.ts | 2 +- .../core/collection/view/manifests.ts | 5 + .../core/collection/view/ref/manifests.ts | 21 ++++ .../view/ref/ref-collection-view.element.ts | 96 +++++++++++++++++ .../packages/core/collection/view/types.ts | 1 + 13 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/manifests.ts rename src/Umbraco.Web.UI.Client/src/packages/core/collection/{extensions => view}/collection-view.extension.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/core/collection/{ => view}/collection-view.manager.test.ts (96%) rename src/Umbraco.Web.UI.Client/src/packages/core/collection/{ => view}/collection-view.manager.ts (97%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/view/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index cb514a6e5075..971c381b055c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -1,5 +1,5 @@ -import { UmbCollectionViewManager } from '../collection-view.manager.js'; -import type { UmbCollectionViewManagerConfig } from '../collection-view.manager.js'; +import { UmbCollectionViewManager } from '../view/collection-view.manager.js'; +import type { UmbCollectionViewManagerConfig } from '../view/collection-view.manager.js'; import type { UmbCollectionColumnConfiguration, UmbCollectionConfiguration, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/types.ts index 4f8fa8382f83..f8326911a22a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/types.ts @@ -1,3 +1,2 @@ export type * from './collection-action.extension.js'; -export type * from './collection-view.extension.js'; export type * from './collection.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/manifests.ts index ee82008f7621..62f625fa4414 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/manifests.ts @@ -3,6 +3,7 @@ import { manifests as actionManifests } from './action/manifests.js'; import { manifests as conditionManifests } from './conditions/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as pickerModalManifests } from './collection-item-picker-modal/manifests.js'; +import { manifests as viewManifests } from './view/manifests.js'; import { manifests as workspaceViewManifests } from './workspace-view/manifests.js'; export const manifests: Array = [ @@ -10,5 +11,6 @@ export const manifests: Array = ...conditionManifests, ...menuManifests, ...pickerModalManifests, + ...viewManifests, ...workspaceViewManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts index 67fb479e88cd..fa8aaf6a342b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts @@ -4,12 +4,13 @@ import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; import type { UmbPaginationManager } from '@umbraco-cms/backoffice/utils'; export type * from './action/create/types.js'; +export type * from './collection-item-picker-modal/types.js'; export type * from './conditions/types.js'; export type * from './extensions/types.js'; export type * from './item/types.js'; export type * from './menu/types.js'; +export type * from './view/types.js'; export type * from './workspace-view/types.js'; -export type * from './collection-item-picker-modal/types.js'; export interface UmbCollectionConfiguration { unique?: UmbEntityUnique; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts new file mode 100644 index 000000000000..1bcad7c9453b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -0,0 +1,102 @@ +import { UMB_COLLECTION_CONTEXT, type UmbCollectionItemModel } from '@umbraco-cms/backoffice/collection'; +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'; + +@customElement('umb-card-collection-view') +export class UmbCardCollectionViewElement extends UmbLitElement { + @state() + private _items: Array = []; + + @state() + private _selection: Array = []; + + @state() + private _loading = false; + + @state() + private _selectOnly: boolean | undefined; + + #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; + + this.observe( + this.#collectionContext?.selection.selection, + (selection) => (this._selection = selection ?? []), + 'umbCollectionSelectionObserver', + ); + + this.observe( + this.#collectionContext?.selection.selectOnly, + (selectOnly) => (this._selectOnly = selectOnly ?? undefined), + 'umbCollectionSelectOnlyObserver', + ); + + this.observe( + this.#collectionContext?.items, + (items) => (this._items = items ?? []), + 'umbCollectionItemsObserver', + ); + }); + } + + #onSelect(item: UmbCollectionItemModel) { + this.#collectionContext?.selection.select(item.unique ?? ''); + } + + #onDeselect(item: UmbCollectionItemModel) { + this.#collectionContext?.selection.deselect(item.unique ?? ''); + } + + override render() { + if (this._loading) return nothing; + return html` +
+ ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderItem(item), + )} +
+ `; + } + + #renderItem(item: UmbCollectionItemModel) { + return html` 0 || this._selectOnly} + ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} + @selected=${() => this.#onSelect(item)} + @deselected=${() => this.#onDeselect(item)}>`; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: flex; + flex-direction: column; + } + + #card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--uui-size-space-4); + } + `, + ]; +} + +export { UmbCardCollectionViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-card-collection-view': UmbCardCollectionViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/manifests.ts new file mode 100644 index 000000000000..b3b5e67bd9d7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/manifests.ts @@ -0,0 +1,21 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'kind', + alias: 'Umb.Kind.CollectionView.Card', + matchKind: 'card', + matchType: 'collectionView', + manifest: { + type: 'collectionView', + kind: 'card', + element: () => import('./card-collection-view.element.js'), + weight: 800, + meta: { + label: 'Cards', + icon: 'icon-grid', + pathName: 'cards', + }, + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/collection-view.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/collection-view.extension.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/collection/extensions/collection-view.extension.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/collection/view/collection-view.extension.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/collection-view.manager.test.ts similarity index 96% rename from src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/collection/view/collection-view.manager.test.ts index 793b9d034393..507e2bc48a23 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/collection-view.manager.test.ts @@ -1,5 +1,5 @@ -import type { ManifestCollectionView } from './extensions/types.js'; -import { umbExtensionsRegistry } from '../extension-registry/index.js'; +import type { ManifestCollectionView } from './collection-view.extension.js'; +import { umbExtensionsRegistry } from '../../extension-registry/index.js'; import { UmbCollectionViewManager } from './collection-view.manager.js'; import { expect } from '@open-wc/testing'; import { Observable } from '@umbraco-cms/backoffice/external/rxjs'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/collection-view.manager.ts similarity index 97% rename from src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/collection/view/collection-view.manager.ts index bb0134824bde..6b06efa44667 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-view.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/collection-view.manager.ts @@ -1,4 +1,4 @@ -import type { ManifestCollectionView } from './extensions/types.js'; +import type { ManifestCollectionView } from './collection-view.extension.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbExtensionsManifestInitializer, createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/manifests.ts new file mode 100644 index 000000000000..fe4f150180e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/manifests.ts @@ -0,0 +1,5 @@ +import { manifests as cardManifests } from './card/manifests.js'; +import { manifests as refManifests } from './ref/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [...cardManifests, ...refManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/manifests.ts new file mode 100644 index 000000000000..f7b2c0e4516d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/manifests.ts @@ -0,0 +1,21 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'kind', + alias: 'Umb.Kind.CollectionView.Ref', + matchKind: 'ref', + matchType: 'collectionView', + manifest: { + type: 'collectionView', + kind: 'ref', + element: () => import('./ref-collection-view.element.js'), + weight: 800, + meta: { + label: 'List', + icon: 'icon-list', + pathName: 'refs', + }, + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts new file mode 100644 index 000000000000..1306ae76cde3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -0,0 +1,96 @@ +import { UMB_COLLECTION_CONTEXT, type UmbCollectionItemModel } from '@umbraco-cms/backoffice/collection'; +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'; + +@customElement('umb-ref-collection-view') +export class UmbRefCollectionViewElement extends UmbLitElement { + @state() + private _items: Array = []; + + @state() + private _selection: Array = []; + + @state() + private _loading = false; + + @state() + private _selectOnly: boolean | undefined; + + #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; + + this.observe( + this.#collectionContext?.selection.selection, + (selection) => (this._selection = selection ?? []), + 'umbCollectionSelectionObserver', + ); + + this.observe( + this.#collectionContext?.selection.selectOnly, + (selectOnly) => (this._selectOnly = selectOnly ?? undefined), + 'umbCollectionSelectOnlyObserver', + ); + + this.observe( + this.#collectionContext?.items, + (items) => (this._items = items ?? []), + 'umbCollectionItemsObserver', + ); + }); + } + + #onSelect(item: UmbCollectionItemModel) { + this.#collectionContext?.selection.select(item.unique ?? ''); + } + + #onDeselect(item: UmbCollectionItemModel) { + this.#collectionContext?.selection.deselect(item.unique ?? ''); + } + + override render() { + if (this._loading) return nothing; + return html` +
+ ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderItem(item), + )} +
+ `; + } + + #renderItem(item: UmbCollectionItemModel) { + return html` 0 || this._selectOnly} + ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} + @selected=${() => this.#onSelect(item)} + @deselected=${() => this.#onDeselect(item)}>`; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + display: flex; + flex-direction: column; + } + `, + ]; +} + +export { UmbRefCollectionViewElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-ref-collection-view': UmbRefCollectionViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts new file mode 100644 index 000000000000..5645aa9330e4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts @@ -0,0 +1 @@ +export type * from '../view/collection-view.extension.js'; From 86485f4b55f14b735ae511105f2d8034431c2b33 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 1 Dec 2025 12:52:53 +0100 Subject: [PATCH 20/48] Utilise card kind for user collection view --- .../src/packages/user/user/collection/views/manifests.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/manifests.ts index 7a5c81ff7f12..f9ce4c576c80 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/manifests.ts @@ -21,15 +21,9 @@ export const manifests: Array = [ }, { type: 'collectionView', + kind: 'card', alias: UMB_COLLECTION_VIEW_USER_GRID, name: 'User Grid Collection View', - element: () => import('./grid/user-grid-collection-view.element.js'), - weight: 200, - meta: { - label: 'Grid', - icon: 'icon-grid', - pathName: 'grid', - }, conditions: [ { alias: UMB_COLLECTION_ALIAS_CONDITION, From f78175a4b575c3cfadbe475aefb9da4ce4d16044 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 1 Dec 2025 15:46:39 +0100 Subject: [PATCH 21/48] Add item-specific href support to collection views Introduces a requestItemHref method to collection contexts for retrieving item-specific hrefs. Updates card, ref, and user table collection views to use these hrefs, enabling dynamic linking for collection items. Refactors user table name column layout to accept href via value prop instead of constructing it internally. --- .../default/collection-default.context.ts | 10 +++++++++ .../src/packages/core/collection/types.ts | 2 ++ .../view/card/card-collection-view.element.ts | 21 ++++++++++++++++++- .../view/ref/ref-collection-view.element.ts | 21 ++++++++++++++++++- .../collection/user-collection.context.ts | 10 +++++++++ .../user-table-name-column-layout.element.ts | 18 ++++++++-------- .../user-table-collection-view.element.ts | 18 +++++++++++++++- 7 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 971c381b055c..718b616bdd8c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -392,4 +392,14 @@ export class UmbDefaultCollectionContext< public getItems() { return this._items.getValue(); } + + /** + * Returns the href for a specific collection item. + * Override this method in specialized collection contexts to provide item-specific hrefs. + * @param {CollectionItemType} item - The collection item to get the href for. + * @returns {Promise} - The href for the item, or undefined if not available. + */ + public async requestItemHref(item: CollectionItemType): Promise { + return undefined; + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts index fa8aaf6a342b..39ad26a61f7e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts @@ -1,4 +1,5 @@ import type { ManifestCollection } from './extensions/types.js'; +import type { UmbCollectionItemModel } from './item/types.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; import type { UmbPaginationManager } from '@umbraco-cms/backoffice/utils'; @@ -49,6 +50,7 @@ export interface UmbCollectionContext { setManifest(manifest: ManifestCollection): void; getManifest(): ManifestCollection | undefined; requestCollection(): Promise; + requestItemHref?(item: UmbCollectionItemModel): Promise; pagination: UmbPaginationManager; items: Observable; totalItems: Observable; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index 1bcad7c9453b..06b7521cc89a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -17,6 +17,9 @@ export class UmbCardCollectionViewElement extends UmbLitElement { @state() private _selectOnly: boolean | undefined; + @state() + private _itemHrefs: Map = new Map(); + #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; constructor() { @@ -39,7 +42,10 @@ export class UmbCardCollectionViewElement extends UmbLitElement { this.observe( this.#collectionContext?.items, - (items) => (this._items = items ?? []), + async (items) => { + this._items = items ?? []; + await this.#updateItemHrefs(); + }, 'umbCollectionItemsObserver', ); }); @@ -53,6 +59,17 @@ export class UmbCardCollectionViewElement extends UmbLitElement { this.#collectionContext?.selection.deselect(item.unique ?? ''); } + async #updateItemHrefs() { + const hrefs = new Map(); + for (const item of this._items) { + const href = await this.#collectionContext?.requestItemHref?.(item); + if (href && item.unique) { + hrefs.set(item.unique, href); + } + } + this._itemHrefs = hrefs; + } + override render() { if (this._loading) return nothing; return html` @@ -67,8 +84,10 @@ export class UmbCardCollectionViewElement extends UmbLitElement { } #renderItem(item: UmbCollectionItemModel) { + const href = item.unique ? this._itemHrefs.get(item.unique) : undefined; return html` 0 || this._selectOnly} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index 1306ae76cde3..1b74ebf2308b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -17,6 +17,9 @@ export class UmbRefCollectionViewElement extends UmbLitElement { @state() private _selectOnly: boolean | undefined; + @state() + private _itemHrefs: Map = new Map(); + #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; constructor() { @@ -39,7 +42,10 @@ export class UmbRefCollectionViewElement extends UmbLitElement { this.observe( this.#collectionContext?.items, - (items) => (this._items = items ?? []), + async (items) => { + this._items = items ?? []; + await this.#updateItemHrefs(); + }, 'umbCollectionItemsObserver', ); }); @@ -53,6 +59,17 @@ export class UmbRefCollectionViewElement extends UmbLitElement { this.#collectionContext?.selection.deselect(item.unique ?? ''); } + async #updateItemHrefs() { + const hrefs = new Map(); + for (const item of this._items) { + const href = await this.#collectionContext?.requestItemHref?.(item); + if (href && item.unique) { + hrefs.set(item.unique, href); + } + } + this._itemHrefs = hrefs; + } + override render() { if (this._loading) return nothing; return html` @@ -67,8 +84,10 @@ export class UmbRefCollectionViewElement extends UmbLitElement { } #renderItem(item: UmbCollectionItemModel) { + const href = item.unique ? this._itemHrefs.get(item.unique) : undefined; return html` 0 || this._selectOnly} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts index 1c0e58775561..18e9864aa761 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts @@ -1,4 +1,5 @@ import type { UmbUserDetailModel } from '../types.js'; +import { UMB_USER_WORKSPACE_PATH } from '../paths.js'; import { UMB_COLLECTION_VIEW_USER_GRID } from './views/index.js'; import type { UmbUserCollectionFilterModel, UmbUserOrderByOption } from './types.js'; import type { UmbUserOrderByType, UmbUserStateFilterType } from './utils/index.js'; @@ -120,6 +121,15 @@ export class UmbUserCollectionContext extends UmbDefaultCollectionContext< setOrderDirectionFilter(orderDirection: UmbDirectionType) { this.setFilter({ orderDirection }); } + + /** + * Returns the href for a specific user collection item. + * @param {UmbUserDetailModel} item - The user item to get the href for. + * @returns {Promise} - The edit workspace href for the user. + */ + override async requestItemHref(item: UmbUserDetailModel): Promise { + return `${UMB_USER_WORKSPACE_PATH}/edit/${item.unique}`; + } } export { UmbUserCollectionContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts index 15292958c7b0..4dd642b73526 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts @@ -1,6 +1,5 @@ -import { UMB_USER_WORKSPACE_PATH } from '../../../../../paths.js'; import { html, LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbTableColumn, UmbTableItem } from '@umbraco-cms/backoffice/components'; +import type { UmbTableColumn } from '@umbraco-cms/backoffice/components'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @customElement('umb-user-table-name-column-layout') @@ -8,22 +7,23 @@ export class UmbUserTableNameColumnLayoutElement extends LitElement { @property({ type: Object, attribute: false }) column!: UmbTableColumn; - @property({ type: Object, attribute: false }) - item!: UmbTableItem; - @property({ attribute: false }) - value!: any; + value!: { + name: string; + unique: string; + kind: string; + avatarUrls: Record; + href?: string; + }; override render() { - const href = UMB_USER_WORKSPACE_PATH + '/edit/' + this.value.unique; - return html` `; } static override styles = [UmbTextStyles]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts index 3c1915636e67..123636a81ee0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts @@ -69,6 +69,9 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { @state() private _selection: Array = []; + @state() + private _itemHrefs: Map = new Map(); + #collectionContext?: UmbUserCollectionContext; constructor() { @@ -83,8 +86,9 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { ); this.observe( this.#collectionContext?.items, - (items) => { + async (items) => { this._users = items ?? []; + await this.#updateItemHrefs(); this.#observeUserGroups(); }, 'umbCollectionItemsObserver', @@ -116,6 +120,17 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { .join(', '); } + async #updateItemHrefs() { + const hrefs = new Map(); + for (const user of this._users) { + const href = await this.#collectionContext?.requestItemHref?.(user); + if (href && user.unique) { + hrefs.set(user.unique, href); + } + } + this._itemHrefs = hrefs; + } + #createTableItems() { this._tableItems = this._users.map((user) => { return { @@ -129,6 +144,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { name: user.name, avatarUrls: user.avatarUrls, kind: user.kind, + href: user.unique ? this._itemHrefs.get(user.unique) : undefined, }, }, { From 1c8cf78acbc988a32a9f100c02f1efda7c0cc52d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 1 Dec 2025 20:43:38 +0100 Subject: [PATCH 22/48] Update ManifestCollectionView import path Changed the import of ManifestCollectionView from '../extensions/types.js' to '../view/types.js' to reflect its new location. --- .../collection/components/collection-view-bundle.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts index c9e115dc9302..fc61f6cb8c92 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts @@ -1,5 +1,5 @@ import { UMB_COLLECTION_CONTEXT } from '../default/index.js'; -import type { ManifestCollectionView } from '../extensions/types.js'; +import type { ManifestCollectionView } from '../view/types.js'; import type { UmbCollectionLayoutConfiguration } from '../types.js'; import { css, customElement, html, nothing, query, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; From 20fb3c9220545ea790f2b66f343783a5f942437c Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 2 Dec 2025 20:23:32 +0100 Subject: [PATCH 23/48] use box --- .../core/collection/view/ref/ref-collection-view.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index 1b74ebf2308b..7f58a16a1012 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -73,13 +73,13 @@ export class UmbRefCollectionViewElement extends UmbLitElement { override render() { if (this._loading) return nothing; return html` -
+ ${repeat( this._items, (item) => item.unique, (item) => this.#renderItem(item), )} -
+ `; } From b0a8fa7f7c0bd29300075de5eaf87116582e4cad Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 3 Dec 2025 21:52:10 +0100 Subject: [PATCH 24/48] render entity actions --- .../collection/view/card/card-collection-view.element.ts | 7 ++++++- .../collection/view/ref/ref-collection-view.element.ts | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index 06b7521cc89a..d5c143284bea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -92,7 +92,12 @@ export class UmbCardCollectionViewElement extends UmbLitElement { ?select-only=${this._selection.length > 0 || this._selectOnly} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} @selected=${() => this.#onSelect(item)} - @deselected=${() => this.#onDeselect(item)}>
`; + @deselected=${() => this.#onDeselect(item)}> + + `; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index 7f58a16a1012..a3566f06ad13 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -92,7 +92,12 @@ export class UmbRefCollectionViewElement extends UmbLitElement { ?select-only=${this._selection.length > 0 || this._selectOnly} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} @selected=${() => this.#onSelect(item)} - @deselected=${() => this.#onDeselect(item)}>`; + @deselected=${() => this.#onDeselect(item)}> + + `; } static override styles = [ From a476ba38eb56775b86e0bbe132bf5a6ec366b67b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 09:58:27 +0100 Subject: [PATCH 25/48] use edit path builder for user links --- .../packages/user/user/collection/user-collection.context.ts | 4 ++-- src/Umbraco.Web.UI.Client/src/packages/user/user/paths.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts index 18e9864aa761..f517c9248c7a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts @@ -1,5 +1,5 @@ import type { UmbUserDetailModel } from '../types.js'; -import { UMB_USER_WORKSPACE_PATH } from '../paths.js'; +import { UMB_EDIT_USER_WORKSPACE_PATH_PATTERN } from '../paths.js'; import { UMB_COLLECTION_VIEW_USER_GRID } from './views/index.js'; import type { UmbUserCollectionFilterModel, UmbUserOrderByOption } from './types.js'; import type { UmbUserOrderByType, UmbUserStateFilterType } from './utils/index.js'; @@ -128,7 +128,7 @@ export class UmbUserCollectionContext extends UmbDefaultCollectionContext< * @returns {Promise} - The edit workspace href for the user. */ override async requestItemHref(item: UmbUserDetailModel): Promise { - return `${UMB_USER_WORKSPACE_PATH}/edit/${item.unique}`; + return `${UMB_EDIT_USER_WORKSPACE_PATH_PATTERN.generateAbsolute({ unique: item.unique })}`; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/paths.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/paths.ts index bc01820db67c..b42a977d6649 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/paths.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/paths.ts @@ -13,4 +13,7 @@ export const UMB_USER_ROOT_WORKSPACE_PATH = UMB_WORKSPACE_PATH_PATTERN.generateA entityType: UMB_USER_ROOT_ENTITY_TYPE, }); -export const UMB_EDIT_USER_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ unique: string }>('edit/:unique'); +export const UMB_EDIT_USER_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ unique: string }>( + 'edit/:unique', + UMB_USER_WORKSPACE_PATH, +); From 4df577688e8836f414cd2516f4b6ec30c0fdf90e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 10:32:29 +0100 Subject: [PATCH 26/48] rename method --- .../default/collection-default.context.ts | 2 +- .../src/packages/core/collection/types.ts | 2 +- .../view/card/card-collection-view.element.ts | 20 +++++++++---------- .../view/ref/ref-collection-view.element.ts | 2 +- .../collection/user-collection.context.ts | 2 +- .../user-table-name-column-layout.element.ts | 4 ++-- .../user-table-collection-view.element.ts | 16 +++++++-------- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 718b616bdd8c..d75d0307ffc1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -399,7 +399,7 @@ export class UmbDefaultCollectionContext< * @param {CollectionItemType} item - The collection item to get the href for. * @returns {Promise} - The href for the item, or undefined if not available. */ - public async requestItemHref(item: CollectionItemType): Promise { + public async requestItemEditPath(item: CollectionItemType): Promise { return undefined; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts index 39ad26a61f7e..325f35c45718 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts @@ -50,7 +50,7 @@ export interface UmbCollectionContext { setManifest(manifest: ManifestCollection): void; getManifest(): ManifestCollection | undefined; requestCollection(): Promise; - requestItemHref?(item: UmbCollectionItemModel): Promise; + requestItemEditPath?(item: UmbCollectionItemModel): Promise; pagination: UmbPaginationManager; items: Observable; totalItems: Observable; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index d5c143284bea..bf79559b7837 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -18,7 +18,7 @@ export class UmbCardCollectionViewElement extends UmbLitElement { private _selectOnly: boolean | undefined; @state() - private _itemHrefs: Map = new Map(); + private _itemEditPaths: Map = new Map(); #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; @@ -44,7 +44,7 @@ export class UmbCardCollectionViewElement extends UmbLitElement { this.#collectionContext?.items, async (items) => { this._items = items ?? []; - await this.#updateItemHrefs(); + await this.#updateItemEditPaths(); }, 'umbCollectionItemsObserver', ); @@ -59,15 +59,15 @@ export class UmbCardCollectionViewElement extends UmbLitElement { this.#collectionContext?.selection.deselect(item.unique ?? ''); } - async #updateItemHrefs() { - const hrefs = new Map(); + async #updateItemEditPaths() { + const editPaths = new Map(); for (const item of this._items) { - const href = await this.#collectionContext?.requestItemHref?.(item); - if (href && item.unique) { - hrefs.set(item.unique, href); + const editPath = await this.#collectionContext?.requestItemEditPath?.(item); + if (editPath && item.unique) { + editPaths.set(item.unique, editPath); } } - this._itemHrefs = hrefs; + this._itemEditPaths = editPaths; } override render() { @@ -84,10 +84,10 @@ export class UmbCardCollectionViewElement extends UmbLitElement { } #renderItem(item: UmbCollectionItemModel) { - const href = item.unique ? this._itemHrefs.get(item.unique) : undefined; + const editPath = item.unique ? this._itemEditPaths.get(item.unique) : undefined; return html` 0 || this._selectOnly} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index a3566f06ad13..df574525e2fa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -62,7 +62,7 @@ export class UmbRefCollectionViewElement extends UmbLitElement { async #updateItemHrefs() { const hrefs = new Map(); for (const item of this._items) { - const href = await this.#collectionContext?.requestItemHref?.(item); + const href = await this.#collectionContext?.requestItemEditPath?.(item); if (href && item.unique) { hrefs.set(item.unique, href); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts index f517c9248c7a..6b2b0865e2dc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts @@ -127,7 +127,7 @@ export class UmbUserCollectionContext extends UmbDefaultCollectionContext< * @param {UmbUserDetailModel} item - The user item to get the href for. * @returns {Promise} - The edit workspace href for the user. */ - override async requestItemHref(item: UmbUserDetailModel): Promise { + override async requestItemEditPath(item: UmbUserDetailModel): Promise { return `${UMB_EDIT_USER_WORKSPACE_PATH_PATTERN.generateAbsolute({ unique: item.unique })}`; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts index 4dd642b73526..4fa2356a757c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts @@ -13,7 +13,7 @@ export class UmbUserTableNameColumnLayoutElement extends LitElement { unique: string; kind: string; avatarUrls: Record; - href?: string; + editPath?: string; }; override render() { @@ -23,7 +23,7 @@ export class UmbUserTableNameColumnLayoutElement extends LitElement { name=${this.value.name} kind=${this.value.kind} .imgUrls=${this.value.avatarUrls}> - ${this.value.name} + ${this.value.name} `; } static override styles = [UmbTextStyles]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts index 123636a81ee0..97f333789c90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts @@ -70,7 +70,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { private _selection: Array = []; @state() - private _itemHrefs: Map = new Map(); + private _itemEditPaths: Map = new Map(); #collectionContext?: UmbUserCollectionContext; @@ -88,7 +88,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { this.#collectionContext?.items, async (items) => { this._users = items ?? []; - await this.#updateItemHrefs(); + await this.#updateItemEditPaths(); this.#observeUserGroups(); }, 'umbCollectionItemsObserver', @@ -120,15 +120,15 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { .join(', '); } - async #updateItemHrefs() { - const hrefs = new Map(); + async #updateItemEditPaths() { + const editPaths = new Map(); for (const user of this._users) { - const href = await this.#collectionContext?.requestItemHref?.(user); + const href = await this.#collectionContext?.requestItemEditPath?.(user); if (href && user.unique) { - hrefs.set(user.unique, href); + editPaths.set(user.unique, href); } } - this._itemHrefs = hrefs; + this._itemEditPaths = editPaths; } #createTableItems() { @@ -144,7 +144,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { name: user.name, avatarUrls: user.avatarUrls, kind: user.kind, - href: user.unique ? this._itemHrefs.get(user.unique) : undefined, + editPath: user.unique ? this._itemEditPaths.get(user.unique) : undefined, }, }, { From 3de2bdf0241e5886842fca7a5251e08f6c827563 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 12:02:38 +0100 Subject: [PATCH 27/48] Revert "rename method" This reverts commit 4df577688e8836f414cd2516f4b6ec30c0fdf90e. --- .../default/collection-default.context.ts | 2 +- .../src/packages/core/collection/types.ts | 2 +- .../view/card/card-collection-view.element.ts | 20 +++++++++---------- .../view/ref/ref-collection-view.element.ts | 2 +- .../collection/user-collection.context.ts | 2 +- .../user-table-name-column-layout.element.ts | 4 ++-- .../user-table-collection-view.element.ts | 16 +++++++-------- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index d75d0307ffc1..718b616bdd8c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -399,7 +399,7 @@ export class UmbDefaultCollectionContext< * @param {CollectionItemType} item - The collection item to get the href for. * @returns {Promise} - The href for the item, or undefined if not available. */ - public async requestItemEditPath(item: CollectionItemType): Promise { + public async requestItemHref(item: CollectionItemType): Promise { return undefined; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts index 325f35c45718..39ad26a61f7e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts @@ -50,7 +50,7 @@ export interface UmbCollectionContext { setManifest(manifest: ManifestCollection): void; getManifest(): ManifestCollection | undefined; requestCollection(): Promise; - requestItemEditPath?(item: UmbCollectionItemModel): Promise; + requestItemHref?(item: UmbCollectionItemModel): Promise; pagination: UmbPaginationManager; items: Observable; totalItems: Observable; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index bf79559b7837..d5c143284bea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -18,7 +18,7 @@ export class UmbCardCollectionViewElement extends UmbLitElement { private _selectOnly: boolean | undefined; @state() - private _itemEditPaths: Map = new Map(); + private _itemHrefs: Map = new Map(); #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; @@ -44,7 +44,7 @@ export class UmbCardCollectionViewElement extends UmbLitElement { this.#collectionContext?.items, async (items) => { this._items = items ?? []; - await this.#updateItemEditPaths(); + await this.#updateItemHrefs(); }, 'umbCollectionItemsObserver', ); @@ -59,15 +59,15 @@ export class UmbCardCollectionViewElement extends UmbLitElement { this.#collectionContext?.selection.deselect(item.unique ?? ''); } - async #updateItemEditPaths() { - const editPaths = new Map(); + async #updateItemHrefs() { + const hrefs = new Map(); for (const item of this._items) { - const editPath = await this.#collectionContext?.requestItemEditPath?.(item); - if (editPath && item.unique) { - editPaths.set(item.unique, editPath); + const href = await this.#collectionContext?.requestItemHref?.(item); + if (href && item.unique) { + hrefs.set(item.unique, href); } } - this._itemEditPaths = editPaths; + this._itemHrefs = hrefs; } override render() { @@ -84,10 +84,10 @@ export class UmbCardCollectionViewElement extends UmbLitElement { } #renderItem(item: UmbCollectionItemModel) { - const editPath = item.unique ? this._itemEditPaths.get(item.unique) : undefined; + const href = item.unique ? this._itemHrefs.get(item.unique) : undefined; return html` 0 || this._selectOnly} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index df574525e2fa..a3566f06ad13 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -62,7 +62,7 @@ export class UmbRefCollectionViewElement extends UmbLitElement { async #updateItemHrefs() { const hrefs = new Map(); for (const item of this._items) { - const href = await this.#collectionContext?.requestItemEditPath?.(item); + const href = await this.#collectionContext?.requestItemHref?.(item); if (href && item.unique) { hrefs.set(item.unique, href); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts index 6b2b0865e2dc..f517c9248c7a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts @@ -127,7 +127,7 @@ export class UmbUserCollectionContext extends UmbDefaultCollectionContext< * @param {UmbUserDetailModel} item - The user item to get the href for. * @returns {Promise} - The edit workspace href for the user. */ - override async requestItemEditPath(item: UmbUserDetailModel): Promise { + override async requestItemHref(item: UmbUserDetailModel): Promise { return `${UMB_EDIT_USER_WORKSPACE_PATH_PATTERN.generateAbsolute({ unique: item.unique })}`; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts index 4fa2356a757c..4dd642b73526 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts @@ -13,7 +13,7 @@ export class UmbUserTableNameColumnLayoutElement extends LitElement { unique: string; kind: string; avatarUrls: Record; - editPath?: string; + href?: string; }; override render() { @@ -23,7 +23,7 @@ export class UmbUserTableNameColumnLayoutElement extends LitElement { name=${this.value.name} kind=${this.value.kind} .imgUrls=${this.value.avatarUrls}> - ${this.value.name} + ${this.value.name} `; } static override styles = [UmbTextStyles]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts index 97f333789c90..123636a81ee0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts @@ -70,7 +70,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { private _selection: Array = []; @state() - private _itemEditPaths: Map = new Map(); + private _itemHrefs: Map = new Map(); #collectionContext?: UmbUserCollectionContext; @@ -88,7 +88,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { this.#collectionContext?.items, async (items) => { this._users = items ?? []; - await this.#updateItemEditPaths(); + await this.#updateItemHrefs(); this.#observeUserGroups(); }, 'umbCollectionItemsObserver', @@ -120,15 +120,15 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { .join(', '); } - async #updateItemEditPaths() { - const editPaths = new Map(); + async #updateItemHrefs() { + const hrefs = new Map(); for (const user of this._users) { - const href = await this.#collectionContext?.requestItemEditPath?.(user); + const href = await this.#collectionContext?.requestItemHref?.(user); if (href && user.unique) { - editPaths.set(user.unique, href); + hrefs.set(user.unique, href); } } - this._itemEditPaths = editPaths; + this._itemHrefs = hrefs; } #createTableItems() { @@ -144,7 +144,7 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { name: user.name, avatarUrls: user.avatarUrls, kind: user.kind, - editPath: user.unique ? this._itemEditPaths.get(user.unique) : undefined, + href: user.unique ? this._itemHrefs.get(user.unique) : undefined, }, }, { From c927d1bb7906b42752f784cf267cd5f1daf5df4f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 12:04:15 +0100 Subject: [PATCH 28/48] Update collection-default.context.ts --- .../core/collection/default/collection-default.context.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 718b616bdd8c..5d628b730a83 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -396,10 +396,9 @@ export class UmbDefaultCollectionContext< /** * Returns the href for a specific collection item. * Override this method in specialized collection contexts to provide item-specific hrefs. - * @param {CollectionItemType} item - The collection item to get the href for. - * @returns {Promise} - The href for the item, or undefined if not available. + * @returns {Promise} - Undefined. The collection item do not link to anything by default. */ - public async requestItemHref(item: CollectionItemType): Promise { + public async requestItemHref(): Promise { return undefined; } } From b220a6987841d8f971ce5daa8a7943e341049d04 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 12:24:42 +0100 Subject: [PATCH 29/48] make type lint ignore unused args with an underscore --- src/Umbraco.Web.UI.Client/eslint.config.js | 13 +++++++++---- .../default/collection-default.context.ts | 9 +++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/eslint.config.js b/src/Umbraco.Web.UI.Client/eslint.config.js index d4cd670cc84a..36116fbd6821 100644 --- a/src/Umbraco.Web.UI.Client/eslint.config.js +++ b/src/Umbraco.Web.UI.Client/eslint.config.js @@ -89,7 +89,12 @@ export default [ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + }, + ], '@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-import-type-side-effects': 'warn', @@ -154,7 +159,7 @@ export default [ selector: 'typeLike', modifiers: ['exported'], format: ['PascalCase'], - prefix: ['Umb', 'Ufm', 'Manifest', 'Meta', 'Example'] + prefix: ['Umb', 'Ufm', 'Manifest', 'Meta', 'Example'], }, // All exported string constants should be UPPER_CASE with leading 'UMB_' // Example: UMB_EXAMPLE_CONSTANT @@ -167,8 +172,8 @@ export default [ }, // Allow destructured variables to be named as they are in the object { - selector: "variable", - modifiers: ["destructured"], + selector: 'variable', + modifiers: ['destructured'], format: null, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 5d628b730a83..41191d532be8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -376,7 +376,7 @@ export class UmbDefaultCollectionContext< * @memberof UmbCollectionContext * @deprecated Use the `.manifest` property instead. */ - public getManifest() { + public getManifest(): ManifestCollection | undefined { new UmbDeprecation({ removeInVersion: '18.0.0', deprecated: 'getManifest', @@ -389,16 +389,17 @@ export class UmbDefaultCollectionContext< * Returns the items in the collection. * @returns {Array} - The items in the collection. */ - public getItems() { + public getItems(): Array { return this._items.getValue(); } /** * Returns the href for a specific collection item. * Override this method in specialized collection contexts to provide item-specific hrefs. - * @returns {Promise} - Undefined. The collection item do not link to anything by default. + * @param {CollectionItemType} _item - The collection item to get the href for. + * @returns {Promise} - Undefined. The collection item do not link to anything by default. */ - public async requestItemHref(): Promise { + public async requestItemHref(_item: CollectionItemType): Promise { return undefined; } } From 0eceae4c7e11e74c9a2e7ded34cfa3e9e855a2a2 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 12:30:13 +0100 Subject: [PATCH 30/48] temp remove unused --- .../view/card/card-collection-view.element.ts | 11 +---------- .../view/ref/ref-collection-view.element.ts | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index d5c143284bea..edaccc5689b6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -14,9 +14,6 @@ export class UmbCardCollectionViewElement extends UmbLitElement { @state() private _loading = false; - @state() - private _selectOnly: boolean | undefined; - @state() private _itemHrefs: Map = new Map(); @@ -34,12 +31,6 @@ export class UmbCardCollectionViewElement extends UmbLitElement { 'umbCollectionSelectionObserver', ); - this.observe( - this.#collectionContext?.selection.selectOnly, - (selectOnly) => (this._selectOnly = selectOnly ?? undefined), - 'umbCollectionSelectOnlyObserver', - ); - this.observe( this.#collectionContext?.items, async (items) => { @@ -89,7 +80,7 @@ export class UmbCardCollectionViewElement extends UmbLitElement { .item=${item} href=${href ?? nothing} selectable - ?select-only=${this._selection.length > 0 || this._selectOnly} + ?select-only=${this._selection.length > 0} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} @selected=${() => this.#onSelect(item)} @deselected=${() => this.#onDeselect(item)}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index a3566f06ad13..11a8fdd6bb0e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -14,9 +14,6 @@ export class UmbRefCollectionViewElement extends UmbLitElement { @state() private _loading = false; - @state() - private _selectOnly: boolean | undefined; - @state() private _itemHrefs: Map = new Map(); @@ -34,12 +31,6 @@ export class UmbRefCollectionViewElement extends UmbLitElement { 'umbCollectionSelectionObserver', ); - this.observe( - this.#collectionContext?.selection.selectOnly, - (selectOnly) => (this._selectOnly = selectOnly ?? undefined), - 'umbCollectionSelectOnlyObserver', - ); - this.observe( this.#collectionContext?.items, async (items) => { @@ -89,7 +80,7 @@ export class UmbRefCollectionViewElement extends UmbLitElement { .item=${item} href=${href ?? nothing} selectable - ?select-only=${this._selection.length > 0 || this._selectOnly} + ?select-only=${this._selection.length > 0} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} @selected=${() => this.#onSelect(item)} @deselected=${() => this.#onDeselect(item)}> From 231477d271998f565bbf37f7a57ed031025e1844 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 13:56:51 +0100 Subject: [PATCH 31/48] only make collection vie selectable if there are any registered bulk actions --- .../collection-bulk-action.manager.test.ts | 73 +++++++++++++++++++ .../collection-bulk-action.manager.ts | 39 ++++++++++ .../default/collection-default.context.ts | 2 + .../view/card/card-collection-view.element.ts | 13 +++- .../view/ref/ref-collection-view.element.ts | 13 +++- .../language-table-collection-view.element.ts | 15 ++++ 6 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.test.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.test.ts new file mode 100644 index 000000000000..b40ad81cafab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.test.ts @@ -0,0 +1,73 @@ +import { UmbCollectionBulkActionManager } from './collection-bulk-action.manager.js'; +import { umbExtensionsRegistry } from '../../extension-registry/index.js'; +import { expect } from '@open-wc/testing'; +import { Observable, first } from '@umbraco-cms/backoffice/external/rxjs'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; + +@customElement('test-bulk-action-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +const bulkActionManifests: Array = [ + { + type: 'entityBulkAction', + alias: 'Umb.Test.EntityBulkAction.1', + name: 'Test Entity Bulk Action 1', + forEntityTypes: ['test-entity'], + }, +]; + +describe('UmbCollectionBulkActionManager', () => { + let hostElement: UmbTestControllerHostElement; + let manager: UmbCollectionBulkActionManager; + + beforeEach(() => { + hostElement = new UmbTestControllerHostElement(); + manager = new UmbCollectionBulkActionManager(hostElement); + }); + + afterEach(() => { + manager.destroy(); + hostElement.destroy(); + }); + + describe('Public API', () => { + describe('properties', () => { + it('has a hasBulkActions property', () => { + expect(manager).to.have.property('hasBulkActions').to.be.an.instanceOf(Observable); + }); + }); + }); + + describe('hasBulkActions', () => { + afterEach(() => { + umbExtensionsRegistry.clear(); + }); + + it('it emits false if there are no actions', (done) => { + // Use first() to only get the initial emission and auto-unsubscribe + manager.hasBulkActions.subscribe((value) => { + expect(value).to.equal(false); + done(); + }); + }); + + it('it emits true if there are actions', (done) => { + let isFirstValue = true; + + // First, we need to add bulk action manifests to the registry + umbExtensionsRegistry.registerMany(bulkActionManifests); + + manager.hasBulkActions.subscribe((value) => { + if (isFirstValue) { + // Skip the first emission which is false + isFirstValue = false; + return; + } + + expect(value).to.equal(true); + done(); + }); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.ts new file mode 100644 index 000000000000..b7b3282ec4bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.ts @@ -0,0 +1,39 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +/** + * Manager responsible for tracking the availability of bulk actions in a collection. + */ +export class UmbCollectionBulkActionManager extends UmbControllerBase { + #hasBulkActions = new UmbBooleanState(undefined); + + /** + * Observable that emits `true` if bulk actions are available, `false` if none are registered, + * or `undefined` if the state has not yet been determined. + */ + public readonly hasBulkActions = this.#hasBulkActions.asObservable(); + + /** + * Creates a new instance of the bulk action manager. + * @param {UmbControllerHost} host - The controller host that owns this manager. + */ + constructor(host: UmbControllerHost) { + super(host); + this.#observeBulkActions(); + } + + #observeBulkActions() { + new UmbExtensionsManifestInitializer( + this, + umbExtensionsRegistry, + 'entityBulkAction', + null, + (bulkActionControllers) => { + this.#hasBulkActions.setValue(bulkActionControllers.length > 0); + }, + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 41191d532be8..9e661858e094 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -9,6 +9,7 @@ import type { import type { UmbCollectionFilterModel } from '../collection-filter-model.interface.js'; import type { UmbCollectionRepository } from '../repository/collection-repository.interface.js'; import type { ManifestCollection } from '../extensions/types.js'; +import { UmbCollectionBulkActionManager } from '../bulk-action/collection-bulk-action.manager.js'; import { UMB_COLLECTION_CONTEXT } from './collection-default.context-token.js'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbArrayState, UmbBasicState, UmbNumberState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; @@ -67,6 +68,7 @@ export class UmbDefaultCollectionContext< public readonly pagination = new UmbPaginationManager(); public readonly selection = new UmbSelectionManager(this); public readonly view = new UmbCollectionViewManager(this); + public readonly bulkAction = new UmbCollectionBulkActionManager(this); #defaultViewAlias: string; #defaultFilter: Partial; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index edaccc5689b6..2132a48f16ed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -17,6 +17,9 @@ export class UmbCardCollectionViewElement extends UmbLitElement { @state() private _itemHrefs: Map = new Map(); + @state() + private _hasBulkActions = false; + #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; constructor() { @@ -31,6 +34,14 @@ export class UmbCardCollectionViewElement extends UmbLitElement { 'umbCollectionSelectionObserver', ); + this.observe( + this.#collectionContext?.bulkAction.hasBulkActions, + (hasBulkActions) => { + this._hasBulkActions = hasBulkActions ?? false; + }, + 'umbCollectionHasBulkActionsObserver', + ); + this.observe( this.#collectionContext?.items, async (items) => { @@ -79,7 +90,7 @@ export class UmbCardCollectionViewElement extends UmbLitElement { return html` 0} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} @selected=${() => this.#onSelect(item)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index 11a8fdd6bb0e..043a085fc7d5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -17,6 +17,9 @@ export class UmbRefCollectionViewElement extends UmbLitElement { @state() private _itemHrefs: Map = new Map(); + @state() + private _hasBulkActions = false; + #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; constructor() { @@ -31,6 +34,14 @@ export class UmbRefCollectionViewElement extends UmbLitElement { 'umbCollectionSelectionObserver', ); + this.observe( + this.#collectionContext?.bulkAction.hasBulkActions, + (hasBulkActions) => { + this._hasBulkActions = hasBulkActions ?? false; + }, + 'umbCollectionHasBulkActionsObserver', + ); + this.observe( this.#collectionContext?.items, async (items) => { @@ -79,7 +90,7 @@ export class UmbRefCollectionViewElement extends UmbLitElement { return html` 0} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} @selected=${() => this.#onSelect(item)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts index b347db3e8196..fa0d61a12533 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts @@ -57,6 +57,7 @@ export class UmbLanguageTableCollectionViewElement extends UmbLitElement { this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { this.#collectionContext = instance; this.#observeCollectionItems(); + this.#observeHasBulkActions(); }); } @@ -65,6 +66,20 @@ export class UmbLanguageTableCollectionViewElement extends UmbLitElement { this.observe(this.#collectionContext.items, (items) => this.#createTableItems(items), 'umbCollectionItemsObserver'); } + #observeHasBulkActions() { + if (!this.#collectionContext) return; + this.observe( + this.#collectionContext.bulkAction.hasBulkActions, + (hasBulkActions) => { + this._tableConfig = { + ...this._tableConfig, + allowSelection: hasBulkActions ?? false, + }; + }, + 'umbCollectionHasBulkActionsObserver', + ); + } + #createTableItems(languages: Array) { this._tableItems = languages.map((language) => { return { From fdc9386207a63782b00e0084f87269f01c044660 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 14:16:47 +0100 Subject: [PATCH 32/48] don't render name link if there is no href --- .../default-collection-item-card.element.ts | 3 ++- .../default-collection-item-ref.element.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 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 d4a1f4bd755e..f1d0ca9b2fc6 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 @@ -49,7 +49,8 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement implement ?selected=${this.selected} ?disabled=${this.disabled} @selected=${this.#onSelected} - @deselected=${this.#onDeselected}> + @deselected=${this.#onDeselected} + ?readonly=${!this.href}> ${this.#renderIcon(this.item)}
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 1ec6db16279e..e529e9fd2fc1 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,7 @@ export class UmbDefaultCollectionItemRefElement extends UmbLitElement implements override render() { if (!this.item) return nothing; - return html` + href=${ifDefined(this.href)} + ?readonly=${!this.href}> ${this.#renderIcon(this.item)} `; From 8e42259b1410d1653ad6bd681905af2c47993770 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 14:19:16 +0100 Subject: [PATCH 33/48] fix imports --- .../core/collection/view/card/card-collection-view.element.ts | 3 ++- .../core/collection/view/ref/ref-collection-view.element.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index 2132a48f16ed..2dd7dc02092c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -1,4 +1,5 @@ -import { UMB_COLLECTION_CONTEXT, type UmbCollectionItemModel } from '@umbraco-cms/backoffice/collection'; +import { UMB_COLLECTION_CONTEXT } from '../../default/index.js'; +import type { UmbCollectionItemModel } from '../../types.js'; 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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index 043a085fc7d5..46c4b3baf946 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -1,4 +1,5 @@ -import { UMB_COLLECTION_CONTEXT, type UmbCollectionItemModel } from '@umbraco-cms/backoffice/collection'; +import { UMB_COLLECTION_CONTEXT } from '../../default/index.js'; +import type { UmbCollectionItemModel } from '../../types.js'; 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'; From 8b0f9847ff6eb8e7db116cf776166335df2af3ae Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 15:18:41 +0100 Subject: [PATCH 34/48] use selectable state --- .../default/collection-default.context.ts | 13 +++++++++++ .../view/card/card-collection-view.element.ts | 19 ++++++++-------- .../view/ref/ref-collection-view.element.ts | 22 +++++++++---------- .../language-table-collection-view.element.ts | 18 ++++++++------- 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 9e661858e094..350c763f584b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -93,6 +93,19 @@ export class UmbDefaultCollectionContext< this.#defaultViewAlias = defaultViewAlias; this.#defaultFilter = defaultFilter; + this.selection.setSelectable(false); + + this.observe( + this.bulkAction.hasBulkActions, + (hasBulkActions) => { + // Allow selection if there are bulk actions available + if (hasBulkActions) { + this.selection.setSelectable(true); + } + }, + 'umbCollectionHasBulkActionsObserver', + ); + this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange); this.#listenToEntityEvents(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index 2dd7dc02092c..087fe7fff974 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -9,6 +9,9 @@ export class UmbCardCollectionViewElement extends UmbLitElement { @state() private _items: Array = []; + @state() + private _selectable = false; + @state() private _selection: Array = []; @@ -30,17 +33,15 @@ export class UmbCardCollectionViewElement extends UmbLitElement { this.#collectionContext = instance; this.observe( - this.#collectionContext?.selection.selection, - (selection) => (this._selection = selection ?? []), - 'umbCollectionSelectionObserver', + this.#collectionContext?.selection.selectable, + (selectable) => (this._selectable = selectable ?? false), + 'umbCollectionSelectableObserver', ); this.observe( - this.#collectionContext?.bulkAction.hasBulkActions, - (hasBulkActions) => { - this._hasBulkActions = hasBulkActions ?? false; - }, - 'umbCollectionHasBulkActionsObserver', + this.#collectionContext?.selection.selection, + (selection) => (this._selection = selection ?? []), + 'umbCollectionSelectionObserver', ); this.observe( @@ -91,7 +92,7 @@ export class UmbCardCollectionViewElement extends UmbLitElement { return html` 0} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} @selected=${() => this.#onSelect(item)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts index 46c4b3baf946..8da047435b2b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/ref/ref-collection-view.element.ts @@ -9,6 +9,9 @@ export class UmbRefCollectionViewElement extends UmbLitElement { @state() private _items: Array = []; + @state() + private _selectable = false; + @state() private _selection: Array = []; @@ -18,9 +21,6 @@ export class UmbRefCollectionViewElement extends UmbLitElement { @state() private _itemHrefs: Map = new Map(); - @state() - private _hasBulkActions = false; - #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; constructor() { @@ -30,17 +30,15 @@ export class UmbRefCollectionViewElement extends UmbLitElement { this.#collectionContext = instance; this.observe( - this.#collectionContext?.selection.selection, - (selection) => (this._selection = selection ?? []), - 'umbCollectionSelectionObserver', + this.#collectionContext?.selection.selectable, + (selectable) => (this._selectable = selectable ?? false), + 'umbCollectionSelectableObserver', ); this.observe( - this.#collectionContext?.bulkAction.hasBulkActions, - (hasBulkActions) => { - this._hasBulkActions = hasBulkActions ?? false; - }, - 'umbCollectionHasBulkActionsObserver', + this.#collectionContext?.selection.selection, + (selection) => (this._selection = selection ?? []), + 'umbCollectionSelectionObserver', ); this.observe( @@ -91,7 +89,7 @@ export class UmbRefCollectionViewElement extends UmbLitElement { return html` 0} ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} @selected=${() => this.#onSelect(item)} diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts index fa0d61a12533..69b5c433f228 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts @@ -57,23 +57,25 @@ export class UmbLanguageTableCollectionViewElement extends UmbLitElement { this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { this.#collectionContext = instance; this.#observeCollectionItems(); - this.#observeHasBulkActions(); + this.#observeIsSelectable(); }); } #observeCollectionItems() { - if (!this.#collectionContext) return; - this.observe(this.#collectionContext.items, (items) => this.#createTableItems(items), 'umbCollectionItemsObserver'); + this.observe( + this.#collectionContext?.items, + (items) => this.#createTableItems(items || []), + 'umbCollectionItemsObserver', + ); } - #observeHasBulkActions() { - if (!this.#collectionContext) return; + #observeIsSelectable() { this.observe( - this.#collectionContext.bulkAction.hasBulkActions, - (hasBulkActions) => { + this.#collectionContext?.selection.selectable, + (isSelectable) => { this._tableConfig = { ...this._tableConfig, - allowSelection: hasBulkActions ?? false, + allowSelection: isSelectable ?? false, }; }, 'umbCollectionHasBulkActionsObserver', From 43ee3884710524a1f5ece3b15801719180fc1c49 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 15:31:10 +0100 Subject: [PATCH 35/48] Update language-table-collection-view.element.ts --- .../views/table/language-table-collection-view.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts index 69b5c433f228..00e6170f9ecb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/collection/views/table/language-table-collection-view.element.ts @@ -78,7 +78,7 @@ export class UmbLanguageTableCollectionViewElement extends UmbLitElement { allowSelection: isSelectable ?? false, }; }, - 'umbCollectionHasBulkActionsObserver', + 'umbCollectionIsSelectableObserver', ); } From 650dcec25e261ac29b34c183fd6918d4b96c164a Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 4 Dec 2025 16:05:52 +0100 Subject: [PATCH 36/48] Update card-collection-view.element.ts --- .../core/collection/view/card/card-collection-view.element.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index 087fe7fff974..02339ac03d61 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -21,9 +21,6 @@ export class UmbCardCollectionViewElement extends UmbLitElement { @state() private _itemHrefs: Map = new Map(); - @state() - private _hasBulkActions = false; - #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; constructor() { From 608e338b715c8936a10239abccb0c391bfcb1000 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 09:25:05 +0100 Subject: [PATCH 37/48] clean up --- .../default/collection-default.context.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 350c763f584b..595b02b44b6e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -93,19 +93,6 @@ export class UmbDefaultCollectionContext< this.#defaultViewAlias = defaultViewAlias; this.#defaultFilter = defaultFilter; - this.selection.setSelectable(false); - - this.observe( - this.bulkAction.hasBulkActions, - (hasBulkActions) => { - // Allow selection if there are bulk actions available - if (hasBulkActions) { - this.selection.setSelectable(true); - } - }, - 'umbCollectionHasBulkActionsObserver', - ); - this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange); this.#listenToEntityEvents(); @@ -178,7 +165,7 @@ export class UmbDefaultCollectionContext< protected _configure() { if (!this.#config) return; - this.selection.setMultiple(true); + this.#configureSelection(); if (this.#config.pageSize) { this.pagination.setPageSize(this.#config.pageSize); @@ -209,6 +196,26 @@ export class UmbDefaultCollectionContext< this._configured = true; } + #configureSelection() { + // TODO: We need support a collecion selection configuration here so ex. Pickers can turn on single and multi select and set a selection. + this.selection.setSelectable(false); + this.selection.setMultiple(false); + + // Observe bulk actions to enable selection when bulk actions are available + // Bulk Actions are an integrated part of a Collection so we handle it here instead of a configuration + this.observe( + this.bulkAction.hasBulkActions, + (hasBulkActions) => { + // Allow selection if there are bulk actions available + if (hasBulkActions) { + this.selection.setSelectable(true); + this.selection.setMultiple(true); + } + }, + 'umbCollectionHasBulkActionsObserver', + ); + } + #checkIfInitialized() { if (this._repository) { this.#initialized = true; @@ -373,7 +380,7 @@ export class UmbDefaultCollectionContext< /** * Sets the manifest for the collection. - * @param {ManifestCollection} manifest + * @param {ManifestCollection} manifest - The manifest for the collection. * @memberof UmbCollectionContext * @deprecated Use set the `.manifest` property instead. */ @@ -387,7 +394,7 @@ export class UmbDefaultCollectionContext< /** * Returns the manifest for the collection. - * @returns {ManifestCollection} + * @returns {ManifestCollection} - The manifest for the collection. * @memberof UmbCollectionContext * @deprecated Use the `.manifest` property instead. */ From 543ef21273ccb93e46eb65c6610cd04d6a946f13 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 11:13:28 +0100 Subject: [PATCH 38/48] Refactor collection views to use shared base class --- .../view/card/card-collection-view.element.ts | 78 ++-------------- .../view/ref/ref-collection-view.element.ts | 78 ++-------------- .../view/umb-collection-view-element-base.ts | 92 +++++++++++++++++++ 3 files changed, 104 insertions(+), 144 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/collection/view/umb-collection-view-element-base.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts index 02339ac03d61..4b3309a74bc2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/card/card-collection-view.element.ts @@ -1,76 +1,10 @@ -import { UMB_COLLECTION_CONTEXT } from '../../default/index.js'; +import { UmbCollectionViewElementBase } from '../umb-collection-view-element-base.js'; import type { UmbCollectionItemModel } from '../../types.js'; -import { css, customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { css, customElement, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @customElement('umb-card-collection-view') -export class UmbCardCollectionViewElement extends UmbLitElement { - @state() - private _items: Array = []; - - @state() - private _selectable = false; - - @state() - private _selection: Array = []; - - @state() - private _loading = false; - - @state() - private _itemHrefs: Map = new Map(); - - #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; - - constructor() { - super(); - - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance; - - this.observe( - this.#collectionContext?.selection.selectable, - (selectable) => (this._selectable = selectable ?? false), - 'umbCollectionSelectableObserver', - ); - - this.observe( - this.#collectionContext?.selection.selection, - (selection) => (this._selection = selection ?? []), - 'umbCollectionSelectionObserver', - ); - - this.observe( - this.#collectionContext?.items, - async (items) => { - this._items = items ?? []; - await this.#updateItemHrefs(); - }, - 'umbCollectionItemsObserver', - ); - }); - } - - #onSelect(item: UmbCollectionItemModel) { - this.#collectionContext?.selection.select(item.unique ?? ''); - } - - #onDeselect(item: UmbCollectionItemModel) { - this.#collectionContext?.selection.deselect(item.unique ?? ''); - } - - async #updateItemHrefs() { - const hrefs = new Map(); - for (const item of this._items) { - const href = await this.#collectionContext?.requestItemHref?.(item); - if (href && item.unique) { - hrefs.set(item.unique, href); - } - } - this._itemHrefs = hrefs; - } - +export class UmbCardCollectionViewElement extends UmbCollectionViewElementBase { override render() { if (this._loading) return nothing; return html` @@ -91,9 +25,9 @@ export class UmbCardCollectionViewElement extends UmbLitElement { href=${href ?? nothing} ?selectable=${this._selectable} ?select-only=${this._selection.length > 0} - ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} - @selected=${() => this.#onSelect(item)} - @deselected=${() => this.#onDeselect(item)}> + ?selected=${this._isSelectedItem(item.unique)} + @selected=${() => this._selectItem(item.unique)} + @deselected=${() => this._deselectItem(item.unique)}> = []; - - @state() - private _selectable = false; - - @state() - private _selection: Array = []; - - @state() - private _loading = false; - - @state() - private _itemHrefs: Map = new Map(); - - #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; - - constructor() { - super(); - - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance; - - this.observe( - this.#collectionContext?.selection.selectable, - (selectable) => (this._selectable = selectable ?? false), - 'umbCollectionSelectableObserver', - ); - - this.observe( - this.#collectionContext?.selection.selection, - (selection) => (this._selection = selection ?? []), - 'umbCollectionSelectionObserver', - ); - - this.observe( - this.#collectionContext?.items, - async (items) => { - this._items = items ?? []; - await this.#updateItemHrefs(); - }, - 'umbCollectionItemsObserver', - ); - }); - } - - #onSelect(item: UmbCollectionItemModel) { - this.#collectionContext?.selection.select(item.unique ?? ''); - } - - #onDeselect(item: UmbCollectionItemModel) { - this.#collectionContext?.selection.deselect(item.unique ?? ''); - } - - async #updateItemHrefs() { - const hrefs = new Map(); - for (const item of this._items) { - const href = await this.#collectionContext?.requestItemHref?.(item); - if (href && item.unique) { - hrefs.set(item.unique, href); - } - } - this._itemHrefs = hrefs; - } - +export class UmbRefCollectionViewElement extends UmbCollectionViewElementBase { override render() { if (this._loading) return nothing; return html` @@ -91,9 +25,9 @@ export class UmbRefCollectionViewElement extends UmbLitElement { href=${href ?? nothing} ?selectable=${this._selectable} ?select-only=${this._selection.length > 0} - ?selected=${this.#collectionContext?.selection.isSelected(item.unique)} - @selected=${() => this.#onSelect(item)} - @deselected=${() => this.#onDeselect(item)}> + ?selected=${this._isSelectedItem(item.unique)} + @selected=${() => this._selectItem(item.unique)} + @deselected=${() => this._deselectItem(item.unique)}> = []; + + @state() + protected _selectable = false; + + @state() + protected _selection: Array = []; + + @state() + protected _loading = false; + + @state() + protected _itemHrefs: Map = new Map(); + + #collectionContext?: typeof UMB_COLLECTION_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; + + this.observe( + this.#collectionContext?.selection.selectable, + (selectable) => (this._selectable = selectable ?? false), + 'umbCollectionSelectableObserver', + ); + + this.observe( + this.#collectionContext?.selection.selection, + (selection) => (this._selection = selection ?? []), + 'umbCollectionSelectionObserver', + ); + + this.observe( + this.#collectionContext?.items, + async (items) => { + this._items = items ?? []; + await this._updateItemHrefs(); + }, + 'umbCollectionItemsObserver', + ); + }); + } + + /** + * Selects an item in the collection. + * @param {string} unique - The unique identifier of the item to select. + */ + protected _selectItem(unique: UmbCollectionItemModel['unique']) { + this.#collectionContext?.selection.select(unique); + } + + /** + * Deselects an item in the collection. + * @param {string} unique - The unique identifier of the item to deselect. + */ + protected _deselectItem(unique: UmbCollectionItemModel['unique']) { + this.#collectionContext?.selection.deselect(unique); + } + + /** + * Checks if an item is currently selected. + * @param {string} unique - The unique identifier of the item to check. + * @returns {boolean} True if the item is selected, false otherwise. + */ + protected _isSelectedItem(unique: UmbCollectionItemModel['unique']): boolean { + return this.#collectionContext?.selection.isSelected(unique) ?? false; + } + + protected async _updateItemHrefs() { + const hrefs = new Map(); + for (const item of this._items) { + const href = await this.#collectionContext?.requestItemHref?.(item); + if (href && item.unique) { + hrefs.set(item.unique, href); + } + } + this._itemHrefs = hrefs; + } +} From 70caf831b2cd261e59749b39c14f7214b0831a09 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 11:30:59 +0100 Subject: [PATCH 39/48] refactor(collection): parallelize href fetching and make method private --- .../view/umb-collection-view-element-base.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/umb-collection-view-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/umb-collection-view-element-base.ts index fd3f9b76115d..e3540cb8922c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/umb-collection-view-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/umb-collection-view-element-base.ts @@ -47,7 +47,7 @@ export abstract class UmbCollectionViewElementBase extends UmbLitElement { this.#collectionContext?.items, async (items) => { this._items = items ?? []; - await this._updateItemHrefs(); + await this.#updateItemHrefs(); }, 'umbCollectionItemsObserver', ); @@ -79,14 +79,13 @@ export abstract class UmbCollectionViewElementBase extends UmbLitElement { return this.#collectionContext?.selection.isSelected(unique) ?? false; } - protected async _updateItemHrefs() { - const hrefs = new Map(); - for (const item of this._items) { - const href = await this.#collectionContext?.requestItemHref?.(item); - if (href && item.unique) { - hrefs.set(item.unique, href); - } - } - this._itemHrefs = hrefs; + async #updateItemHrefs() { + const entries = await Promise.all( + this._items.map(async (item) => { + const href = await this.#collectionContext?.requestItemHref?.(item); + return item.unique && href ? ([item.unique, href] as const) : null; + }), + ); + this._itemHrefs = new Map(entries.filter((entry): entry is [string, string] => entry !== null)); } } From f911ce3ee6c07b9b5952013507bc5a03621eb3ed Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 11:46:06 +0100 Subject: [PATCH 40/48] docs(examples): update collection example to use card and ref kinds --- .../card-view/collection-view.element.ts | 82 ------------------- .../collection/card-view/manifests.ts | 8 +- .../collection/collection/manifests.ts | 2 + .../collection/ref-view/manifests.ts | 17 ++++ 4 files changed, 20 insertions(+), 89 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/collection-view.element.ts create mode 100644 src/Umbraco.Web.UI.Client/examples/collection/collection/ref-view/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/collection-view.element.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/collection-view.element.ts deleted file mode 100644 index 64a49f207366..000000000000 --- a/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/collection-view.element.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { ExampleCollectionItemModel } from '../repository/types.js'; -import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; -import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; - -@customElement('example-card-collection-view') -export class ExampleCardCollectionViewElement extends UmbLitElement { - @state() - private _items: Array = []; - - #collectionContext?: UmbDefaultCollectionContext; - - constructor() { - super(); - - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance; - this.#observeCollectionItems(); - }); - } - - #observeCollectionItems() { - this.observe(this.#collectionContext?.items, (items) => (this._items = items || []), 'umbCollectionItemsObserver'); - } - - override render() { - return html` -
- ${repeat( - this._items, - (item) => item.unique, - (item) => - html` - -
${item.name}
-
`, - )} -
- `; - } - - static override styles = [ - UmbTextStyles, - css` - :host { - display: flex; - flex-direction: column; - } - - #card-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - grid-auto-rows: 200px; - gap: var(--uui-size-space-5); - } - - uui-card { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - height: 100%; - - uui-icon { - font-size: 2em; - margin-bottom: var(--uui-size-space-4); - } - } - `, - ]; -} - -export { ExampleCardCollectionViewElement as element }; - -declare global { - interface HTMLElementTagNameMap { - 'example-card-collection-view': ExampleCardCollectionViewElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/manifests.ts index bf7299c5308d..036f43689601 100644 --- a/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/manifests.ts +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/card-view/manifests.ts @@ -4,15 +4,9 @@ import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collecti export const manifests: Array = [ { type: 'collectionView', + kind: 'card', alias: 'Example.CollectionView.Card', name: 'Example Card Collection View', - js: () => import('./collection-view.element.js'), - weight: 50, - meta: { - label: 'Card', - icon: 'icon-grid', - pathName: 'card', - }, conditions: [ { alias: UMB_COLLECTION_ALIAS_CONDITION, diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/manifests.ts index eb8b44d061e8..bd1161dbe4ba 100644 --- a/src/Umbraco.Web.UI.Client/examples/collection/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/manifests.ts @@ -1,6 +1,7 @@ import { EXAMPLE_COLLECTION_ALIAS } from './constants.js'; import { EXAMPLE_COLLECTION_REPOSITORY_ALIAS } from './repository/constants.js'; import { manifests as cardViewManifests } from './card-view/manifests.js'; +import { manifests as refViewManifests } from './ref-view/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as tableViewManifests } from './table-view/manifests.js'; @@ -15,6 +16,7 @@ export const manifests: Array = [ }, }, ...cardViewManifests, + ...refViewManifests, ...repositoryManifests, ...tableViewManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/ref-view/manifests.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/ref-view/manifests.ts new file mode 100644 index 000000000000..f8886aa3e64b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/ref-view/manifests.ts @@ -0,0 +1,17 @@ +import { EXAMPLE_COLLECTION_ALIAS } from '../constants.js'; +import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; + +export const manifests: Array = [ + { + type: 'collectionView', + kind: 'ref', + alias: 'Example.CollectionView.Ref', + name: 'Example Ref Collection View', + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: EXAMPLE_COLLECTION_ALIAS, + }, + ], + }, +]; From 1ee69b202010d614ad21c3f216190281abf42887 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 12:04:42 +0100 Subject: [PATCH 41/48] docs(examples): add icon property to collection example data model --- .../collection/repository/collection.repository.ts | 5 +++++ .../examples/collection/collection/repository/types.ts | 1 + .../collection/table-view/collection-view.element.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/collection.repository.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/collection.repository.ts index ecd8a1495cf7..a6d7e9879849 100644 --- a/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/collection.repository.ts +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/collection.repository.ts @@ -16,26 +16,31 @@ export class ExampleCollectionRepository unique: '3e31e9c5-7d66-4c99-a9e5-d9f2b1e2b22f', entityType: 'example', name: 'Example Item 1', + icon: 'icon-newspaper', }, { unique: 'bc9b6e24-4b11-4dd6-8d4e-7c4f70e59f3c', entityType: 'example', name: 'Example Item 2', + icon: 'icon-newspaper', }, { unique: '5a2f4e3a-ef7e-470e-8c3c-3d859c02ae0d', entityType: 'example', name: 'Example Item 3', + icon: 'icon-newspaper', }, { unique: 'f4c3d8b8-6d79-4c87-9aa9-56b1d8fda702', entityType: 'example', name: 'Example Item 4', + icon: 'icon-newspaper', }, { unique: 'c9f0a8a3-1b49-4724-bde3-70e31592eb6e', entityType: 'example', name: 'Example Item 5', + icon: 'icon-newspaper', }, ]; diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/types.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/types.ts index 4430638c1697..3ee3f404026d 100644 --- a/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/types.ts +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/repository/types.ts @@ -4,6 +4,7 @@ export interface ExampleCollectionItemModel { unique: string; entityType: string; name: string; + icon: string; } export interface ExampleCollectionFilterModel extends UmbCollectionFilterModel {} diff --git a/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/collection-view.element.ts b/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/collection-view.element.ts index 530ee846ddbc..2f466531232e 100644 --- a/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/examples/collection/collection/table-view/collection-view.element.ts @@ -52,7 +52,7 @@ export class ExampleTableCollectionViewElement extends UmbLitElement { this._tableItems = items.map((item) => { return { id: item.unique, - icon: 'icon-newspaper', + icon: item.icon, data: [ { columnAlias: 'name', From da75061de7db7eeaff766851c3bc88d9be2309eb Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 14:58:49 +0100 Subject: [PATCH 42/48] Update src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../core/collection/default/collection-default.context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 595b02b44b6e..951d165fed80 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -197,7 +197,7 @@ export class UmbDefaultCollectionContext< } #configureSelection() { - // TODO: We need support a collecion selection configuration here so ex. Pickers can turn on single and multi select and set a selection. + // TODO: We need support a collection selection configuration here so ex. Pickers can turn on single and multi select and set a selection. this.selection.setSelectable(false); this.selection.setMultiple(false); From 7e3153915ef30e814a5fa0d1a9d72470320dcf90 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 14:59:09 +0100 Subject: [PATCH 43/48] Update src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../core/collection/default/collection-default.context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts index 951d165fed80..58fbbab84147 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts @@ -419,7 +419,7 @@ export class UmbDefaultCollectionContext< * Returns the href for a specific collection item. * Override this method in specialized collection contexts to provide item-specific hrefs. * @param {CollectionItemType} _item - The collection item to get the href for. - * @returns {Promise} - Undefined. The collection item do not link to anything by default. + * @returns {Promise} - Undefined. The collection item does not link to anything by default. */ public async requestItemHref(_item: CollectionItemType): Promise { return undefined; From 2af23c145b8797d605e6be1a7630923a2c2f886d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 15:01:40 +0100 Subject: [PATCH 44/48] Update src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/packages/core/collection/view/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts index 5645aa9330e4..a508a7ab213e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/view/types.ts @@ -1 +1 @@ -export type * from '../view/collection-view.extension.js'; +export type * from './collection-view.extension.js'; From 7c1091f327be6b5b3bad80e8f04cfba64f66e109 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 15:02:22 +0100 Subject: [PATCH 45/48] Update collection-bulk-action.manager.test.ts --- .../bulk-action/collection-bulk-action.manager.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.test.ts index b40ad81cafab..bed586483d65 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/bulk-action/collection-bulk-action.manager.test.ts @@ -1,7 +1,7 @@ import { UmbCollectionBulkActionManager } from './collection-bulk-action.manager.js'; import { umbExtensionsRegistry } from '../../extension-registry/index.js'; import { expect } from '@open-wc/testing'; -import { Observable, first } from '@umbraco-cms/backoffice/external/rxjs'; +import { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; import { customElement } from '@umbraco-cms/backoffice/external/lit'; @@ -45,7 +45,6 @@ describe('UmbCollectionBulkActionManager', () => { }); it('it emits false if there are no actions', (done) => { - // Use first() to only get the initial emission and auto-unsubscribe manager.hasBulkActions.subscribe((value) => { expect(value).to.equal(false); done(); From 6d919e00b33b47f6d128af76d4601f1f5d422a5d Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 15:10:17 +0100 Subject: [PATCH 46/48] Removed duplicate and redundant '@typescript-eslint/no-unused-vars' rule definitions, consolidating the configuration to use only 'argsIgnorePattern'. --- src/Umbraco.Web.UI.Client/eslint.config.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/eslint.config.js b/src/Umbraco.Web.UI.Client/eslint.config.js index 36116fbd6821..302b8bcf9078 100644 --- a/src/Umbraco.Web.UI.Client/eslint.config.js +++ b/src/Umbraco.Web.UI.Client/eslint.config.js @@ -86,15 +86,9 @@ export default [ ...importPlugin.flatConfigs.typescript, rules: { 'no-unused-vars': 'off', //Let '@typescript-eslint/no-unused-vars' catch the errors to allow unused function parameters (ex: in interfaces) - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - }, - ], '@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-import-type-side-effects': 'warn', From a0f43ca94fbce0d256711f94f3437be1b85904ee Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 15:18:51 +0100 Subject: [PATCH 47/48] Handle missing user href in name column layout Replaces the user name link with a span when the href property is not provided, preventing broken links in the user table name column layout. --- .../name/user-table-name-column-layout.element.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts index 4dd642b73526..e724fd22775a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts @@ -1,4 +1,4 @@ -import { html, LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { html, LitElement, customElement, property, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import type { UmbTableColumn } from '@umbraco-cms/backoffice/components'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -23,7 +23,10 @@ export class UmbUserTableNameColumnLayoutElement extends LitElement { name=${this.value.name} kind=${this.value.kind} .imgUrls=${this.value.avatarUrls}> - ${this.value.name} + + ${this.value.href + ? html`${this.value.name}` + : html` ${this.value.name}`} `; } static override styles = [UmbTextStyles]; From b1cc623369a84dd561f9b70e7355e23cf41e42ea Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 5 Dec 2025 15:21:38 +0100 Subject: [PATCH 48/48] Update user-table-name-column-layout.element.ts --- .../name/user-table-name-column-layout.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts index e724fd22775a..7e8683be692d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/column-layouts/name/user-table-name-column-layout.element.ts @@ -1,4 +1,4 @@ -import { html, LitElement, customElement, property, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { html, LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import type { UmbTableColumn } from '@umbraco-cms/backoffice/components'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style';