diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts index 6ab13a8459..34328f20b7 100644 --- a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts @@ -1,15 +1,22 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbCollectionFilterModel, UmbCollectionItemModel } from '@umbraco-cms/backoffice/collection'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; import type { UmbPickerCollectionDataSource, UmbPickerSearchableDataSource, } from '@umbraco-cms/backoffice/picker-data-source'; import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +interface ExampleCollectionItemModel extends UmbCollectionItemModel { + isPickable: boolean; +} + export class ExampleCustomPickerCollectionPropertyEditorDataSource extends UmbControllerBase - implements UmbPickerCollectionDataSource, UmbPickerSearchableDataSource + implements UmbPickerCollectionDataSource, UmbPickerSearchableDataSource { + collectionPickableFilter = (item: ExampleCollectionItemModel) => item.isPickable; + async requestCollection(args: UmbCollectionFilterModel) { // TODO: use args to filter/paginate etc console.log(args); @@ -41,35 +48,40 @@ export class ExampleCustomPickerCollectionPropertyEditorDataSource export { ExampleCustomPickerCollectionPropertyEditorDataSource as api }; -const customItems: Array = [ +const customItems: Array = [ { unique: '1', entityType: 'example', name: 'Example 1', icon: 'icon-shape-triangle', + isPickable: true, }, { unique: '2', entityType: 'example', name: 'Example 2', icon: 'icon-shape-triangle', + isPickable: true, }, { unique: '3', entityType: 'example', name: 'Example 3', icon: 'icon-shape-triangle', + isPickable: true, }, { unique: '4', entityType: 'example', name: 'Example 4', icon: 'icon-shape-triangle', + isPickable: false, }, { unique: '5', entityType: 'example', name: 'Example 5', icon: 'icon-shape-triangle', + isPickable: true, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts index 5f32ec4ac1..20dce58e6a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts @@ -9,6 +9,7 @@ export * from './conditions/index.js'; export * from './constants.js'; export * from './default/collection-default.element.js'; export * from './global-components.js'; +export * from './menu/index.js'; export * from './workspace-view/index.js'; export * from './default/collection-default.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts index 5b0fcb8cb6..edcb448557 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts @@ -1 +1,2 @@ export { UMB_COLLECTION_MENU_CONTEXT } from './default/default-collection-menu.context.token.js'; +export * from './menu-item/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts index a1273792f3..c4f74e2085 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts @@ -1,7 +1,6 @@ import type { UmbCollectionItemModel } from '../../item/types.js'; import type { UmbCollectionSelectionConfiguration } from '../../types.js'; import type { UmbDefaultCollectionMenuContext } from './default-collection-menu.context.js'; -import { getItemFallbackIcon, getItemFallbackName } from '@umbraco-cms/backoffice/entity-item'; import { html, customElement, @@ -14,6 +13,8 @@ import { } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import '../menu-item/collection-menu-item.element.js'; + @customElement('umb-default-collection-menu') export class UmbDefaultCollectionMenuElement extends UmbLitElement { private _api: UmbDefaultCollectionMenuContext | undefined; @@ -115,16 +116,11 @@ export class UmbDefaultCollectionMenuElement extends UmbLitElement { #renderItem(item: UmbCollectionItemModel) { return html` - this._api?.selection.select(item.unique)} - @deselected=${() => this._api?.selection.deselect(item.unique)} - ?selected=${this._api?.selection.isSelected(item.unique)}> - ${item.icon - ? html`` - : html``} - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/index.ts new file mode 100644 index 0000000000..91173f9401 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/index.ts @@ -0,0 +1 @@ +export * from './menu-item/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item-context.interface.ts new file mode 100644 index 0000000000..22082ea596 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item-context.interface.ts @@ -0,0 +1,17 @@ +import type { UmbCollectionItemModel } from '../../item/types.js'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbContextMinimal } from '@umbraco-cms/backoffice/context-api'; + +export interface UmbCollectionMenuItemContext< + CollectionMenuItemType extends UmbCollectionItemModel = UmbCollectionItemModel, +> extends UmbApi, + UmbContextMinimal { + item: Observable; + isSelectable: Observable; + isSelected: Observable; + getItem(): CollectionMenuItemType | undefined; + setItem(item: CollectionMenuItemType | undefined): void; + select(): void; + deselect(): void; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.context.token.ts new file mode 100644 index 0000000000..0b36cdf25e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.context.token.ts @@ -0,0 +1,6 @@ +import type { UmbCollectionMenuItemContext } from './collection-menu-item-context.interface.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_COLLECTION_MENU_ITEM_CONTEXT = new UmbContextToken( + 'UmbCollectionMenuItemContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.element.ts new file mode 100644 index 0000000000..90f12f474f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/collection-menu-item.element.ts @@ -0,0 +1,96 @@ +import { UmbDefaultCollectionMenuItemContext } from './default/index.js'; +import type { ManifestCollectionMenuItem } from './extension/types.js'; +import { customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { + UmbExtensionElementAndApiSlotElementBase, + umbExtensionsRegistry, +} from '@umbraco-cms/backoffice/extension-registry'; +import { createObservablePart } from '@umbraco-cms/backoffice/observable-api'; + +@customElement('umb-collection-menu-item') +export class UmbCollectionMenuItemElement extends UmbExtensionElementAndApiSlotElementBase { + @property({ type: String, reflect: true }) + get entityType() { + return this.#entityType; + } + set entityType(newVal) { + this.#entityType = newVal; + this.#observeEntityType(); + } + #entityType?: string; + + @property({ type: Object, attribute: false }) + override set props(newVal: Record | undefined) { + super.props = newVal; + this.#assignProps(); + } + override get props() { + return super.props; + } + + #observeEntityType() { + if (!this.#entityType) return; + + const filterByEntityType = (manifest: ManifestCollectionMenuItem) => { + if (!this.#entityType) return false; + return manifest.forEntityTypes.includes(this.#entityType); + }; + + // Check if we can find a matching collection menu item for the current entity type. + // If we can, we will use that one, if not we will render a fallback collection menu item. + this.observe( + // TODO: what should we do if there are multiple collection menu items for an entity type? + // This method gets all extensions based on a type, then filters them based on the entity type. and then we get the alias of the first one [NL] + createObservablePart( + umbExtensionsRegistry.byTypeAndFilter(this.getExtensionType(), filterByEntityType), + (x) => x[0]?.alias, + ), + (alias) => { + this.alias = alias; + + // If we don't find any registered collection menu items for this specific entity type, we will render a fallback collection menu item. + // This is on purpose not done with the extension initializer since we don't want to spin up a real extension unless we have to. + if (!alias) { + this.#renderFallbackItem(); + } + }, + 'umbObserveAlias', + ); + } + + #renderFallbackItem() { + // TODO: make creating of elements with apis a shared function. + const element = document.createElement('umb-default-collection-menu-item'); + const api = new UmbDefaultCollectionMenuItemContext(element); + element.api = api; + this._element = element; + this.#assignProps(); + this.requestUpdate('_element'); + } + + getExtensionType() { + return 'collectionMenuItem'; + } + + getDefaultElementName() { + return 'umb-default-collection-menu-item'; + } + + #assignProps() { + if (!this._element || !this.props) return; + + Object.keys(this.props).forEach((key) => { + (this._element as any)[key] = this.props![key]; + }); + } + + override getDefaultApiConstructor() { + return UmbDefaultCollectionMenuItemContext; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-collection-menu-item': UmbCollectionMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/constants.ts new file mode 100644 index 0000000000..a238646f6e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/constants.ts @@ -0,0 +1 @@ +export * from './default/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/constants.ts new file mode 100644 index 0000000000..84ef3547b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/constants.ts @@ -0,0 +1 @@ +export { UMB_COLLECTION_MENU_ITEM_DEFAULT_KIND_MANIFEST } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.context.ts new file mode 100644 index 0000000000..a56480c6ae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.context.ts @@ -0,0 +1,114 @@ +import type { UmbCollectionMenuItemContext } from '../collection-menu-item-context.interface.js'; +import { UMB_COLLECTION_MENU_ITEM_CONTEXT } from '../collection-menu-item.context.token.js'; +import type { UmbCollectionItemModel } from '../../../types.js'; +import type { ManifestCollectionMenuItem } from '../extension/types.js'; +import { UMB_COLLECTION_MENU_CONTEXT } from '../../default/default-collection-menu.context.token.js'; +import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { map } from '@umbraco-cms/backoffice/external/rxjs'; + +export class UmbDefaultCollectionMenuItemContext< + CollectionMenuItemType extends UmbCollectionItemModel = UmbCollectionItemModel, + > + extends UmbContextBase + implements UmbCollectionMenuItemContext +{ + #manifest?: ManifestCollectionMenuItem; + + protected readonly _item = new UmbObjectState(undefined); + readonly item = this._item.asObservable(); + + #isSelectable = new UmbBooleanState(false); + readonly isSelectable = this.#isSelectable.asObservable(); + + #isSelectableContext = new UmbBooleanState(false); + readonly isSelectableContext = this.#isSelectableContext.asObservable(); + + #isSelected = new UmbBooleanState(false); + readonly isSelected = this.#isSelected.asObservable(); + + #collectionMenuContext?: typeof UMB_COLLECTION_MENU_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host, UMB_COLLECTION_MENU_ITEM_CONTEXT); + this.#consumeContexts(); + } + + async #consumeContexts() { + this.consumeContext(UMB_COLLECTION_MENU_CONTEXT, (context) => { + this.#collectionMenuContext = context; + this.#observeIsSelectable(); + this.#observeIsSelected(); + }); + } + + public set manifest(manifest: ManifestCollectionMenuItem | undefined) { + if (this.#manifest === manifest) return; + this.#manifest = manifest; + } + public get manifest() { + return this.#manifest; + } + + public setItem(item: CollectionMenuItemType | undefined) { + this._item.setValue(item); + + if (item) { + this.#observeIsSelectable(); + this.#observeIsSelected(); + } + } + + public select() { + const unique = this.getItem()?.unique; + if (!unique) throw new Error('Could not select. Unique is missing'); + this.#collectionMenuContext?.selection.select(unique); + } + + public deselect() { + const unique = this.getItem()?.unique; + if (!unique) throw new Error('Could not deselect. Unique is missing'); + this.#collectionMenuContext?.selection.deselect(unique); + } + + getItem() { + return this._item.getValue(); + } + + #observeIsSelectable() { + if (!this.#collectionMenuContext) return; + const item = this.getItem(); + if (!item) return; + + this.observe( + this.#collectionMenuContext.selection.selectable, + (value) => { + this.#isSelectableContext.setValue(value); + + // If the collection menu is selectable, check if this item is selectable + if (value === true) { + const isSelectable = this.#collectionMenuContext?.selectableFilter?.(item) ?? true; + this.#isSelectable.setValue(isSelectable); + } + }, + 'observeIsSelectable', + ); + } + + #observeIsSelected() { + if (!this.#collectionMenuContext) return; + const unique = this.getItem()?.unique; + if (!unique) return; + + this.observe( + this.#collectionMenuContext.selection.selection.pipe(map((selection) => selection.includes(unique))), + (isSelected) => { + this.#isSelected.setValue(isSelected); + }, + 'observeIsSelected', + ); + } +} + +export { UmbDefaultCollectionMenuItemContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts new file mode 100644 index 0000000000..3dfed63ced --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts @@ -0,0 +1,80 @@ +import type { UmbCollectionItemModel } from '../../../item/types.js'; +import type { UmbCollectionMenuItemContext } from '../collection-menu-item-context.interface.js'; +import { html, state, property, customElement, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { getItemFallbackIcon, getItemFallbackName } from '@umbraco-cms/backoffice/entity-item'; + +@customElement('umb-default-collection-menu-item') +export class UmbDefaultCollectionMenuItemElement extends UmbLitElement { + @property({ type: Object, attribute: false }) + set item(newVal: UmbCollectionItemModel) { + this._item = newVal; + + if (this._item) { + this.#initItem(); + } + } + get item(): UmbCollectionItemModel | undefined { + return this._item; + } + protected _item?: UmbCollectionItemModel; + + @property({ type: Object, attribute: false }) + public set api(value: UmbCollectionMenuItemContext | undefined) { + this.#api = value; + + if (this.#api) { + this.observe(this.#api.isSelectable, (value) => (this._isSelectable = value)); + this.observe(this.#api.isSelected, (value) => (this._isSelected = value)); + this.#initItem(); + } + } + public get api(): UmbCollectionMenuItemContext | undefined { + return this.#api; + } + #api: UmbCollectionMenuItemContext | undefined; + + @state() + protected _isActive = false; + + @state() + protected _isSelected = false; + + @state() + protected _isSelectable = false; + + #initItem() { + if (!this.#api) return; + if (!this._item) return; + this.#api.setItem(this._item); + } + + override render() { + const item = this._item; + if (!item) return nothing; + + return html` + this.#api?.select()} + @deselected=${() => this.#api?.deselect()}> + ${item.icon + ? html`` + : html``} + + `; + } + + static override styles = [UmbTextStyles]; +} + +export { UmbDefaultCollectionMenuItemElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-default-collection-menu-item': UmbDefaultCollectionMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/index.ts new file mode 100644 index 0000000000..8ad34c7c7e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/index.ts @@ -0,0 +1,2 @@ +export { UmbDefaultCollectionMenuItemContext } from './default-collection-menu-item.context.js'; +export { UmbDefaultCollectionMenuItemElement } from './default-collection-menu-item.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/manifests.ts new file mode 100644 index 0000000000..0e55f0d326 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/manifests.ts @@ -0,0 +1,17 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_COLLECTION_MENU_ITEM_DEFAULT_KIND_MANIFEST: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.CollectionMenuItem.Default', + matchKind: 'default', + matchType: 'collectionMenuItem', + manifest: { + type: 'collectionMenuItem', + api: () => import('./default-collection-menu-item.context.js'), + element: () => import('./default-collection-menu-item.element.js'), + }, +}; + +export const manifests: Array = [ + UMB_COLLECTION_MENU_ITEM_DEFAULT_KIND_MANIFEST, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/collection-menu-item.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/collection-menu-item.extension.ts new file mode 100644 index 0000000000..4cc20eb332 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/collection-menu-item.extension.ts @@ -0,0 +1,12 @@ +import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestCollectionMenuItem extends ManifestElementAndApi { + type: 'collectionMenuItem'; + forEntityTypes: Array; +} + +declare global { + interface UmbExtensionManifestMap { + UmbCollectionMenuItem: ManifestCollectionMenuItem; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/types.ts new file mode 100644 index 0000000000..2cd985cb85 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/extension/types.ts @@ -0,0 +1 @@ +export type * from './collection-menu-item.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/index.ts new file mode 100644 index 0000000000..e3d7b85788 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/index.ts @@ -0,0 +1,4 @@ +export * from './default/index.js'; +export * from './collection-menu-item.context.token.js'; +export * from './collection-menu-item.element.js'; +export type * from './collection-menu-item-context.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/manifests.ts new file mode 100644 index 0000000000..43d020c74e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as defaultManifests } from './default/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [...defaultManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts index b45660db04..07bfa14320 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts @@ -1,3 +1,4 @@ export * from './views/constants.js'; export const UMB_USER_COLLECTION_ALIAS = 'Umb.Collection.User'; export { UMB_USER_COLLECTION_CONTEXT } from './user-collection.context-token.js'; +export * from './menu/constants.js'; 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 b54121406d..59374f44b5 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 @@ -1,7 +1,8 @@ import { UMB_USER_COLLECTION_REPOSITORY_ALIAS } from './repository/index.js'; +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 collectionActionManifests } from './action/manifests.js'; import { UMB_USER_COLLECTION_ALIAS } from './constants.js'; export const manifests: Array = [ @@ -15,7 +16,8 @@ export const manifests: Array = [ repositoryAlias: UMB_USER_COLLECTION_REPOSITORY_ALIAS, }, }, + ...collectionActionManifests, + ...collectionMenuManifests, ...collectionRepositoryManifests, ...collectionViewManifests, - ...collectionActionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts new file mode 100644 index 0000000000..9b0faca55e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts @@ -0,0 +1 @@ +export const UMB_USER_COLLECTION_MENU_ALIAS = 'Umb.CollectionMenu.User'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts new file mode 100644 index 0000000000..4c5ede48d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts @@ -0,0 +1,23 @@ +import { UMB_USER_ENTITY_TYPE } from '../../entity.js'; +import { UMB_USER_COLLECTION_REPOSITORY_ALIAS } from '../repository/constants.js'; +import { UMB_USER_COLLECTION_MENU_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'collectionMenu', + kind: 'default', + alias: UMB_USER_COLLECTION_MENU_ALIAS, + name: 'User Collection Menu', + meta: { + collectionRepositoryAlias: UMB_USER_COLLECTION_REPOSITORY_ALIAS, + }, + }, + { + type: 'collectionMenuItem', + kind: 'default', + alias: 'Umb.CollectionMenuItem.User', + name: 'User Collection Menu Item', + element: () => import('./user-collection-menu-item.element.js'), + forEntityTypes: [UMB_USER_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts new file mode 100644 index 0000000000..a0ec66d78a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts @@ -0,0 +1,88 @@ +import type { UmbUserDetailModel } from '../../types.js'; +import { html, customElement, css, state, property, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbCollectionMenuItemContext } from '@umbraco-cms/backoffice/collection'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('umb-user-collection-menu-item') +export class UmbUserCollectionMenuItemElement extends UmbLitElement { + @property({ type: Object, attribute: false }) + set item(newVal: UmbUserDetailModel) { + this._item = newVal; + + if (this._item) { + this.#initItem(); + } + } + get item(): UmbUserDetailModel | undefined { + return this._item; + } + protected _item?: UmbUserDetailModel; + + @property({ type: Object, attribute: false }) + public set api(value: UmbCollectionMenuItemContext | undefined) { + this.#api = value; + + if (this.#api) { + this.observe(this.#api.isSelectable, (value) => (this._isSelectable = value)); + this.observe(this.#api.isSelected, (value) => (this._isSelected = value)); + this.#initItem(); + } + } + public get api(): UmbCollectionMenuItemContext | undefined { + return this.#api; + } + #api: UmbCollectionMenuItemContext | undefined; + + @state() + protected _isActive = false; + + @state() + protected _isSelected = false; + + @state() + protected _isSelectable = false; + + #initItem() { + if (!this.#api) return; + if (!this._item) return; + this.#api.setItem(this._item); + } + + override render() { + const item = this._item; + if (!item) return nothing; + + return html` + this.#api?.select()} + @deselected=${() => this.#api?.deselect()}> + + + `; + } + + static override styles = [ + UmbTextStyles, + css` + umb-user-avatar { + font-size: 10px; + } + `, + ]; +} + +export { UmbUserCollectionMenuItemElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-collection-menu-item': UmbUserCollectionMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts index 03273e88df..ec1100d7a6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts @@ -3,7 +3,6 @@ export const manifests: Array = [ type: 'modal', alias: 'Umb.Modal.User.Picker', name: 'User Picker Modal', - js: () => import('./user-picker/user-picker-modal.element.js'), }, { type: 'modal', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts deleted file mode 100644 index e79f930e88..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { UmbUserCollectionRepository } from '../../collection/repository/user-collection.repository.js'; -import type { UmbUserItemModel } from '../../repository/item/index.js'; -import type { UmbUserPickerModalData, UmbUserPickerModalValue } from './user-picker-modal.token.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; -import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; - -@customElement('umb-user-picker-modal') -export class UmbUserPickerModalElement extends UmbModalBaseElement { - @state() - private _users: Array = []; - - #selectionManager = new UmbSelectionManager(this); - #userCollectionRepository = new UmbUserCollectionRepository(this); - - override connectedCallback(): void { - super.connectedCallback(); - - // TODO: in theory this config could change during the lifetime of the modal, so we could observe it - this.#selectionManager.setMultiple(this.data?.multiple ?? false); - this.#selectionManager.setSelection(this.value?.selection ?? []); - } - - protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void { - super.firstUpdated(_changedProperties); - this.#requestUsers(); - } - - async #requestUsers() { - if (!this.#userCollectionRepository) return; - const { data } = await this.#userCollectionRepository.requestCollection(); - - if (data) { - this._users = data.items; - } - } - - #submit() { - this.value = { selection: this.#selectionManager.getSelection() }; - this.modalContext?.submit(); - } - - #close() { - this.modalContext?.reject(); - } - - override render() { - return html` - - - ${this._users.map( - (user) => html` - this.#selectionManager.select(user.unique)} - @deselected=${() => this.#selectionManager.deselect(user.unique)} - ?selected=${this.#selectionManager.isSelected(user.unique)}> - - - `, - )} - -
- - -
-
- `; - } - - static override styles = [ - UmbTextStyles, - css` - umb-user-avatar { - font-size: 12px; - } - `, - ]; -} - -export default UmbUserPickerModalElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-user-picker-modal': UmbUserPickerModalElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts index a72fc779c8..2f2e030b6c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts @@ -1,19 +1,29 @@ import type { UmbUserDetailModel } from '../../types.js'; -import type { UmbPickerModalData } from '@umbraco-cms/backoffice/modal'; +import { UMB_USER_COLLECTION_MENU_ALIAS } from '../../collection/constants.js'; +import type { + UmbCollectionItemPickerModalData, + UmbCollectionItemPickerModalValue, +} from '@umbraco-cms/backoffice/collection'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -export type UmbUserPickerModalData = UmbPickerModalData; +export type UmbUserPickerModalData = UmbCollectionItemPickerModalData; -export interface UmbUserPickerModalValue { - selection: Array; -} +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbUserPickerModalValue extends UmbCollectionItemPickerModalValue {} export const UMB_USER_PICKER_MODAL = new UmbModalToken( - 'Umb.Modal.User.Picker', + /* TODO: use constant. We had to use the string directly here to avoid a circular dependency. + When we have removed the dataType (dependency on content) from the picker context we update this */ + 'Umb.Modal.CollectionItemPicker', { modal: { type: 'sidebar', size: 'small', }, + data: { + collection: { + menuAlias: UMB_USER_COLLECTION_MENU_ALIAS, + }, + }, }, );