Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cc1c99b
Add entity collection item card extension type + default elements
madsrasmussen Nov 25, 2025
852a9d7
implement user collection item card
madsrasmussen Nov 25, 2025
0dc90ad
fix selection events
madsrasmussen Nov 25, 2025
8c047e0
map to prop
madsrasmussen Nov 25, 2025
f06d594
add prop/attr for href
madsrasmussen Nov 25, 2025
df1219a
add support for which detail properties to show
madsrasmussen Nov 25, 2025
0f59c3e
update type import
madsrasmussen Nov 26, 2025
2aefe5e
Update src/Umbraco.Web.UI.Client/src/packages/core/collection/item/en…
madsrasmussen Nov 26, 2025
37b9df4
import card in correct file
madsrasmussen Nov 26, 2025
3dbe65a
Fix event listener binding for selection events
madsrasmussen Nov 26, 2025
8865403
implement disabled property for collection item cards
madsrasmussen Nov 26, 2025
5c4b2b8
init commit of collection item ref extension
madsrasmussen Nov 26, 2025
4450ae9
fix imports
madsrasmussen Nov 26, 2025
e98822e
add element interface
madsrasmussen Nov 26, 2025
c29817b
Implement UmbEntityCollectionItemElement interface in item cards
madsrasmussen Nov 26, 2025
51f5bfc
Merge branch 'main' into v17/collection-item-ref-extension
madsrasmussen Nov 28, 2025
801f4d6
Update collection item ref to use uui-ref-node
madsrasmussen Nov 28, 2025
d6b1caa
Refactor entity collection item elements to use shared base
madsrasmussen Nov 28, 2025
3e1063c
use class instead of magic string
madsrasmussen Nov 28, 2025
77e6aea
Merge branch 'main' into v17/collection-item-ref-extension
madsrasmussen Dec 2, 2025
77000cf
Use ifDefined for href in item card element
madsrasmussen Dec 2, 2025
3b0eb73
Fix href attribute handling in collection item ref
madsrasmussen Dec 2, 2025
4902698
Make meta property optional in ManifestEntityCollectionItemBase
madsrasmussen Dec 2, 2025
542d679
Use ifDefined for href binding in document card
madsrasmussen Dec 2, 2025
d1b498f
Fix user card href binding with ifDefined
madsrasmussen Dec 2, 2025
06d6cd4
Apply suggestion from @Copilot
madsrasmussen Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 { customElement, html, ifDefined, 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;

Expand Down Expand Up @@ -42,7 +43,7 @@ export class UmbDefaultCollectionItemCardElement extends UmbLitElement {
return html`
<uui-card-content-node
name=${this.item.name ?? `${getItemFallbackName(this.item)}`}
href=${this.href}
href=${ifDefined(this.href)}
?selectable=${this.selectable}
?select-only=${this.selectOnly}
?selected=${this.selected}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,201 +1,29 @@
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 './default-collection-item-card.element.js';
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';

@customElement('umb-entity-collection-item-card')
export class UmbEntityCollectionItemCardElement extends UmbLitElement {
#extensionsController?: UmbExtensionsElementInitializer<any>;
#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 new UmbDefaultCollectionItemCardElement();
}

#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<UmbCollectionItemDetailPropertyConfig>;
@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<any> | Map<PropertyKey, unknown>): 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
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<any>,
ManifestWithDynamicConditions<UmbExtensionConditionConfig> {
> extends ManifestEntityCollectionItemBase<MetaType> {
type: 'entityCollectionItemCard';
meta: MetaType;
forEntityTypes: Array<string>;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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, ifDefined, 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 implements UmbEntityCollectionItemElement {
@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` <uui-ref-node
name=${this.item.name ?? `${getItemFallbackName(this.item)}`}
@selected=${this.#onSelected}
@deselected=${this.#onDeselected}
?selectable=${this.selectable}
?select-only=${this.selectOnly}
?selected=${this.selected}
?disabled=${this.disabled}
href=${ifDefined(this.href)}>
<slot name="actions" slot="actions"></slot>
${this.#renderIcon(this.item)}
</uui-ref-node>`;
}

#renderIcon(item: UmbCollectionItemModel) {
const icon = item.icon || getItemFallbackIcon();
return html`<umb-icon slot="icon" name=${icon}></umb-icon>`;
}
}

declare global {
interface HTMLElementTagNameMap {
'umb-default-collection-item-ref': UmbDefaultCollectionItemRefElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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';

@customElement('umb-entity-collection-item-ref')
export class UmbEntityCollectionItemRefElement extends UmbEntityCollectionItemElementBase {
protected getExtensionType(): string {
return 'entityCollectionItemRef';
}

protected createFallbackElement(): HTMLElement {
return new UmbDefaultCollectionItemRefElement();
}

protected getPathAddendum(entityType: string, unique: string): string {
return 'collection-item-ref/' + entityType + '/' + unique;
}

protected getMarkAttributeName(): string {
return 'entity-collection-item-ref';
}

override render() {
return html`${this._component}`;
}

static override styles = [
css`
:host {
display: block;
position: relative;
}
`,
];
}

declare global {
interface HTMLElementTagNameMap {
'umb-entity-collection-item-ref': UmbEntityCollectionItemRefElement;
}
}
Loading
Loading