Collection menu item extension point (#20506)
* add extension option for collection menu item * Add collection menu module export * remove unused css * register user collection menu item * register user collection menu * use collection modal for user picker * Delete user-picker-modal.element.ts * Update manifests.ts * explicit exports to avoid name collision * hack to avoid circular dependency * fix lint errors * fix missing const export * Update collection-menu-item.element.ts
This commit is contained in:
@@ -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<ExampleCollectionItemModel>, 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<UmbCollectionItemModel> = [
|
||||
const customItems: Array<ExampleCollectionItemModel> = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { UMB_COLLECTION_MENU_CONTEXT } from './default/default-collection-menu.context.token.js';
|
||||
export * from './menu-item/constants.js';
|
||||
|
||||
@@ -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`
|
||||
<uui-menu-item
|
||||
label=${item.name ?? getItemFallbackName(item)}
|
||||
selectable
|
||||
@selected=${() => this._api?.selection.select(item.unique)}
|
||||
@deselected=${() => this._api?.selection.deselect(item.unique)}
|
||||
?selected=${this._api?.selection.isSelected(item.unique)}>
|
||||
${item.icon
|
||||
? html`<uui-icon slot="icon" name=${item.icon}></uui-icon>`
|
||||
: html`<uui-icon slot="icon" name=${getItemFallbackIcon()}></uui-icon>`}
|
||||
</uui-menu-item>
|
||||
<umb-collection-menu-item
|
||||
entityType=${item.entityType}
|
||||
.props=${{
|
||||
item: item,
|
||||
}}></umb-collection-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './menu-item/index.js';
|
||||
@@ -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<CollectionMenuItemType | undefined>;
|
||||
isSelectable: Observable<boolean>;
|
||||
isSelected: Observable<boolean>;
|
||||
getItem(): CollectionMenuItemType | undefined;
|
||||
setItem(item: CollectionMenuItemType | undefined): void;
|
||||
select(): void;
|
||||
deselect(): void;
|
||||
}
|
||||
@@ -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>(
|
||||
'UmbCollectionMenuItemContext',
|
||||
);
|
||||
@@ -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<ManifestCollectionMenuItem> {
|
||||
@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<string, unknown> | 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './default/constants.js';
|
||||
@@ -0,0 +1 @@
|
||||
export { UMB_COLLECTION_MENU_ITEM_DEFAULT_KIND_MANIFEST } from './manifests.js';
|
||||
@@ -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<CollectionMenuItemType>
|
||||
{
|
||||
#manifest?: ManifestCollectionMenuItem;
|
||||
|
||||
protected readonly _item = new UmbObjectState<CollectionMenuItemType | undefined>(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 };
|
||||
@@ -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`
|
||||
<uui-menu-item
|
||||
label=${item?.name ?? getItemFallbackName(item)}
|
||||
?selectable=${this._isSelectable}
|
||||
?selected=${this._isSelected}
|
||||
@selected=${() => this.#api?.select()}
|
||||
@deselected=${() => this.#api?.deselect()}>
|
||||
${item.icon
|
||||
? html`<uui-icon slot="icon" name=${item.icon}></uui-icon>`
|
||||
: html`<uui-icon slot="icon" name=${getItemFallbackIcon()}></uui-icon>`}
|
||||
</uui-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [UmbTextStyles];
|
||||
}
|
||||
|
||||
export { UmbDefaultCollectionMenuItemElement as element };
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-default-collection-menu-item': UmbDefaultCollectionMenuItemElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { UmbDefaultCollectionMenuItemContext } from './default-collection-menu-item.context.js';
|
||||
export { UmbDefaultCollectionMenuItemElement } from './default-collection-menu-item.element.js';
|
||||
@@ -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<UmbExtensionManifest | UmbExtensionManifestKind> = [
|
||||
UMB_COLLECTION_MENU_ITEM_DEFAULT_KIND_MANIFEST,
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface ManifestCollectionMenuItem extends ManifestElementAndApi<any, any> {
|
||||
type: 'collectionMenuItem';
|
||||
forEntityTypes: Array<string>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface UmbExtensionManifestMap {
|
||||
UmbCollectionMenuItem: ManifestCollectionMenuItem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type * from './collection-menu-item.extension.js';
|
||||
@@ -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';
|
||||
@@ -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<UmbExtensionManifest | UmbExtensionManifestKind> = [...defaultManifests];
|
||||
@@ -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';
|
||||
|
||||
@@ -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<UmbExtensionManifest> = [
|
||||
@@ -15,7 +16,8 @@ export const manifests: Array<UmbExtensionManifest> = [
|
||||
repositoryAlias: UMB_USER_COLLECTION_REPOSITORY_ALIAS,
|
||||
},
|
||||
},
|
||||
...collectionActionManifests,
|
||||
...collectionMenuManifests,
|
||||
...collectionRepositoryManifests,
|
||||
...collectionViewManifests,
|
||||
...collectionActionManifests,
|
||||
];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const UMB_USER_COLLECTION_MENU_ALIAS = 'Umb.CollectionMenu.User';
|
||||
@@ -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<UmbExtensionManifest> = [
|
||||
{
|
||||
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],
|
||||
},
|
||||
];
|
||||
@@ -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`
|
||||
<uui-menu-item
|
||||
label=${item?.name}
|
||||
?selectable=${this._isSelectable}
|
||||
?selected=${this._isSelected}
|
||||
@selected=${() => this.#api?.select()}
|
||||
@deselected=${() => this.#api?.deselect()}>
|
||||
<umb-user-avatar
|
||||
slot="icon"
|
||||
.name=${item.name}
|
||||
.kind=${item.kind}
|
||||
.imgUrls=${item.avatarUrls}></umb-user-avatar>
|
||||
</uui-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ export const manifests: Array<UmbExtensionManifest> = [
|
||||
type: 'modal',
|
||||
alias: 'Umb.Modal.User.Picker',
|
||||
name: 'User Picker Modal',
|
||||
js: () => import('./user-picker/user-picker-modal.element.js'),
|
||||
},
|
||||
{
|
||||
type: 'modal',
|
||||
|
||||
@@ -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<UmbUserPickerModalData, UmbUserPickerModalValue> {
|
||||
@state()
|
||||
private _users: Array<UmbUserItemModel> = [];
|
||||
|
||||
#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<any> | Map<PropertyKey, unknown>): 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`
|
||||
<umb-body-layout headline=${this.localize.term('defaultdialogs_chooseUsers')}>
|
||||
<uui-box>
|
||||
${this._users.map(
|
||||
(user) => html`
|
||||
<uui-menu-item
|
||||
label=${ifDefined(user.name)}
|
||||
selectable
|
||||
@selected=${() => this.#selectionManager.select(user.unique)}
|
||||
@deselected=${() => this.#selectionManager.deselect(user.unique)}
|
||||
?selected=${this.#selectionManager.isSelected(user.unique)}>
|
||||
<umb-user-avatar
|
||||
slot="icon"
|
||||
.name=${user.name}
|
||||
.kind=${user.kind}
|
||||
.imgUrls=${user.avatarUrls}></umb-user-avatar>
|
||||
</uui-menu-item>
|
||||
`,
|
||||
)}
|
||||
</uui-box>
|
||||
<div slot="actions">
|
||||
<uui-button label=${this.localize.term('general_close')} @click=${this.#close}></uui-button>
|
||||
<uui-button
|
||||
label=${this.localize.term('general_choose')}
|
||||
look="primary"
|
||||
color="positive"
|
||||
@click=${this.#submit}></uui-button>
|
||||
</div>
|
||||
</umb-body-layout>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
umb-user-avatar {
|
||||
font-size: 12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbUserPickerModalElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-user-picker-modal': UmbUserPickerModalElement;
|
||||
}
|
||||
}
|
||||
@@ -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<UmbUserDetailModel>;
|
||||
export type UmbUserPickerModalData = UmbCollectionItemPickerModalData<UmbUserDetailModel>;
|
||||
|
||||
export interface UmbUserPickerModalValue {
|
||||
selection: Array<string | null>;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface UmbUserPickerModalValue extends UmbCollectionItemPickerModalValue {}
|
||||
|
||||
export const UMB_USER_PICKER_MODAL = new UmbModalToken<UmbUserPickerModalData, UmbUserPickerModalValue>(
|
||||
'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,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user