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..a0ae8323e4fc --- /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,68 @@ +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-card') +export class UmbDefaultCollectionItemCardElement 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` + + + ${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..f7e2774b6830 --- /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,213 @@ +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'; + +@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; + } + } + + #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-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 }, + ); + } + + 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-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..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 @@ -1,7 +1,14 @@ import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +export type * from './entity-collection-item-card/types.js'; export interface UmbCollectionItemModel extends UmbEntityModel { unique: string; name?: string; icon?: string; } + +export interface UmbCollectionItemDetailPropertyConfig { + alias: string; + name: string; + isSystem: boolean; +} 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(); } 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..71c619c6f731 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/item/document-collection-item-card.element.ts @@ -0,0 +1,85 @@ +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 './document-grid-collection-card.element.js'; + +@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: Boolean }) + disabled = 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/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/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..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 { @@ -84,18 +83,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 +109,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/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; 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..f5f52a147441 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/item/user-collection-item-card.element.ts @@ -0,0 +1,166 @@ +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; + + @property({ type: Boolean }) + disabled = false; + + @property({ type: String }) + href?: string; + + @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..a2bf780988bd 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,10 @@ -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 { UMB_USER_WORKSPACE_PATH } from '../../../paths.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'; -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 +17,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 +37,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 +62,14 @@ 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 +86,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 {