Merge pull request #1358 from umbraco/feature/property-action-refactor

refactor property actions
This commit is contained in:
Niels Lyngsø
2024-03-04 16:42:11 +01:00
committed by GitHub
30 changed files with 266 additions and 190 deletions

View File

@@ -122,7 +122,7 @@ export class UmbExtensionElementAndApiInitializer<
// TODO: we could optimize this so we only re-set the updated props.
Object.keys(this.#apiProps).forEach((key) => {
(this.#component as any)[key] = this.#apiProps![key];
(this.#api as any)[key] = this.#apiProps![key];
});
};

View File

@@ -26,7 +26,7 @@ import type { ManifestMenu } from './menu.model.js';
import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.model.js';
import type { ManifestModal } from './modal.model.js';
import type { ManifestPackageView } from './package-view.model.js';
import type { ManifestPropertyAction } from './property-action.model.js';
import type { ManifestPropertyAction, ManifestPropertyActionDefaultKind } from './property-action.model.js';
import type { ManifestPropertyEditorUi, ManifestPropertyEditorSchema } from './property-editor.model.js';
import type { ManifestRepository } from './repository.model.js';
import type { ManifestSection } from './section.model.js';
@@ -109,6 +109,8 @@ export type ManifestEntityActions =
export type ManifestWorkspaceActions = ManifestWorkspaceAction | ManifestWorkspaceActionDefaultKind;
export type ManifestPropertyActions = ManifestPropertyAction | ManifestPropertyActionDefaultKind;
export type ManifestTypes =
| ManifestBundle<ManifestTypes>
| ManifestCondition
@@ -134,7 +136,7 @@ export type ManifestTypes =
| ManifestMenuItemTreeKind
| ManifestModal
| ManifestPackageView
| ManifestPropertyAction
| ManifestPropertyActions
| ManifestPropertyEditorSchema
| ManifestPropertyEditorUi
| ManifestRepository

View File

@@ -1,13 +1,43 @@
import type { ConditionTypes } from '../conditions/types.js';
import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
import type { UmbPropertyAction } from '../../property-action/components/property-action/property-action.interface.js';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestPropertyAction
extends ManifestElement<HTMLElement>,
export interface ManifestPropertyAction<MetaType extends MetaPropertyAction = MetaPropertyAction>
extends ManifestElementAndApi<UmbControllerHostElement, UmbPropertyAction<MetaType>>,
ManifestWithDynamicConditions<ConditionTypes> {
type: 'propertyAction';
meta: MetaPropertyAction;
forPropertyEditorUis: string[];
meta: MetaType;
}
export interface MetaPropertyAction {
propertyEditors: string[];
export interface MetaPropertyAction {}
export interface ManifestPropertyActionDefaultKind<
MetaType extends MetaPropertyActionDefaultKind = MetaPropertyActionDefaultKind,
> extends ManifestPropertyAction<MetaType> {
type: 'propertyAction';
kind: 'default';
}
export interface MetaPropertyActionDefaultKind extends MetaPropertyAction {
/**
* An icon to represent the action to be performed
*
* @examples [
* "icon-box",
* "icon-grid"
* ]
*/
icon: string;
/**
* The friendly name of the action to perform
*
* @examples [
* "Create",
* "Create Content Template"
* ]
*/
label: string;
}

View File

@@ -1,7 +1,7 @@
import type { ConditionTypes } from '../conditions/types.js';
import type { UmbWorkspaceActionMenuItem } from '../../workspace/components/workspace-action-menu-item/workspace-action-menu-item.interface.js';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
import type { UmbWorkspaceActionMenuItem } from '@umbraco-cms/backoffice/workspace';
export interface ManifestWorkspaceActionMenuItem<
MetaType extends MetaWorkspaceActionMenuItem = MetaWorkspaceActionMenuItem,

View File

@@ -0,0 +1,10 @@
import { UmbPropertyActionBase } from '../../components/property-action/property-action-base.controller.js';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
export class UmbClearPropertyAction extends UmbPropertyActionBase {
async execute() {
const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT);
propertyContext.clearValue();
}
}
export default UmbClearPropertyAction;

View File

@@ -1,57 +0,0 @@
import type { UmbPropertyAction } from '../../shared/property-action/property-action.interface.js';
import type { UmbPropertyContext } from '@umbraco-cms/backoffice/property';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-property-action-clear')
export class UmbPropertyActionClearElement extends UmbLitElement implements UmbPropertyAction {
@property()
value = '';
// THESE OUT COMMENTED CODE IS USED FOR THE EXAMPLE BELOW, TODO: Should be transferred to some documentation.
//private _propertyActionMenuContext?: UmbPropertyActionMenuContext;
private _propertyContext?: UmbPropertyContext;
constructor() {
super();
/*
this.consumeContext('umbPropertyActionMenu', (propertyActionsContext: UmbPropertyActionMenuContext) => {
this._propertyActionMenuContext = propertyActionsContext;
});
*/
this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext: UmbPropertyContext) => {
this._propertyContext = propertyContext;
});
}
private _handleLabelClick() {
this._clearValue();
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
// Or you can do this:
//this._propertyActionMenuContext?.close();
}
private _clearValue() {
// TODO: how do we want to update the value? Testing an event based approach. We need to test an api based approach too.
//this.value = '';// This is though bad as it assumes we are dealing with a string. So wouldn't work as a generalized element.
//this.dispatchEvent(new CustomEvent('property-value-change'));
// Or you can do this:
this._propertyContext?.resetValue(); // This resets value to what the property wants.
}
render() {
return html` <uui-menu-item label="Clear" @click-label="${this._handleLabelClick}">
<uui-icon slot="icon" name="delete"></uui-icon>
</uui-menu-item>`;
}
}
export default UmbPropertyActionClearElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-action-clear': UmbPropertyActionClearElement;
}
}

View File

@@ -1,15 +0,0 @@
import type { Meta, Story } from '@storybook/web-components';
import type { UmbPropertyActionClearElement } from './property-action-clear.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './property-action-clear.element.js';
export default {
title: 'Property Actions/Clear',
component: 'umb-property-action-clear',
id: 'umb-property-action-clear',
} as Meta;
export const AAAOverview: Story<UmbPropertyActionClearElement> = () =>
html` <umb-property-action-clear></umb-property-action-clear>`;
AAAOverview.storyName = 'Overview';

View File

@@ -0,0 +1,16 @@
import { UmbPropertyActionBase } from '../../components/property-action/property-action-base.controller.js';
import { UMB_NOTIFICATION_CONTEXT, type UmbNotificationDefaultData } from '@umbraco-cms/backoffice/notification';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
export class UmbCopyPropertyAction extends UmbPropertyActionBase {
async execute() {
const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT);
const value = propertyContext.getValue();
const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
// TODO: Temporary solution to make something happen: [NL]
const data: UmbNotificationDefaultData = { headline: 'Copied to clipboard', message: value };
notificationContext?.peek('positive', { data });
}
}
export default UmbCopyPropertyAction;

View File

@@ -1,48 +0,0 @@
import type { UmbPropertyAction } from '../../shared/property-action/property-action.interface.js';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import type { UmbNotificationDefaultData, UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
//import { UMB_WORKSPACE_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-property-action-copy')
export class UmbPropertyActionCopyElement extends UmbLitElement implements UmbPropertyAction {
@property()
value = '';
private _notificationContext?: UmbNotificationContext;
constructor() {
super();
//this.consumeContext(UMB_WORKSPACE_PROPERTY_CONTEXT, (property) => {
//console.log('Got a reference to the editor element', property.getEditor());
// Be aware that the element might switch, so using the direct reference is not recommended, instead observe the .element Observable()
//});
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => {
this._notificationContext = instance;
});
}
private _handleLabelClick() {
const data: UmbNotificationDefaultData = { message: 'Copied to clipboard' };
this._notificationContext?.peek('positive', { data });
// TODO: how do we want to close the menu? Testing an event based approach
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
}
render() {
return html` <uui-menu-item label="Copy" @click-label="${this._handleLabelClick}">
<uui-icon slot="icon" name="copy"></uui-icon>
</uui-menu-item>`;
}
}
export default UmbPropertyActionCopyElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-action-copy': UmbPropertyActionCopyElement;
}
}

View File

@@ -1,15 +0,0 @@
import type { Meta, Story } from '@storybook/web-components';
import type { UmbPropertyActionCopyElement } from './property-action-copy.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './property-action-copy.element.js';
export default {
title: 'Property Actions/Copy',
component: 'umb-property-action-copy',
id: 'umb-property-action-copy',
} as Meta;
export const AAAOverview: Story<UmbPropertyActionCopyElement> = () =>
html` <umb-property-action-copy></umb-property-action-copy>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,39 +1,35 @@
import type { UmbPropertyActionArgs } from '../property-action/types.js';
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { ManifestPropertyAction, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
import type {
ManifestPropertyAction,
ManifestTypes,
MetaPropertyAction,
} from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api';
import type { UmbExtensionElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionsElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
function ExtensionApiArgsMethod(manifest: ManifestPropertyAction): [UmbPropertyActionArgs<MetaPropertyAction>] {
return [{ meta: manifest.meta }];
}
@customElement('umb-property-action-menu')
export class UmbPropertyActionMenuElement extends UmbLitElement {
#actionsInitializer?: UmbExtensionsElementInitializer<ManifestTypes, 'propertyAction'>;
#value: unknown;
#actionsInitializer?: UmbExtensionsElementAndApiInitializer<ManifestTypes, 'propertyAction'>;
#propertyEditorUiAlias = '';
@property({ attribute: false })
public set value(value: unknown) {
this.#value = value;
if (this.#actionsInitializer) {
this.#actionsInitializer.properties = { value };
}
}
public get value(): unknown {
return this.#value;
}
@property()
set propertyEditorUiAlias(alias: string) {
this.#propertyEditorUiAlias = alias;
// TODO: Stop using string for 'propertyAction', we need to start using Const.
// TODO: Align property actions with entity actions.
this.#actionsInitializer = new UmbExtensionsElementInitializer(
// TODO: Stop using string for 'propertyAction', we need to start using Const. [NL]
this.#actionsInitializer = new UmbExtensionsElementAndApiInitializer(
this,
umbExtensionsRegistry,
'propertyAction',
(propertyAction) => propertyAction.meta.propertyEditors.includes(alias),
ExtensionApiArgsMethod,
(propertyAction) => propertyAction.forPropertyEditorUis.includes(alias),
(ctrls) => {
this._actions = ctrls;
},
@@ -45,7 +41,7 @@ export class UmbPropertyActionMenuElement extends UmbLitElement {
}
@state()
private _actions: Array<UmbExtensionElementInitializer<ManifestPropertyAction, never>> = [];
private _actions: Array<UmbExtensionElementAndApiInitializer<ManifestPropertyAction, never>> = [];
render() {
return this._actions.length > 0
@@ -54,13 +50,17 @@ export class UmbPropertyActionMenuElement extends UmbLitElement {
id="popover-trigger"
popovertarget="property-action-popover"
look="secondary"
label="More"
label="Open actions menu"
compact>
<uui-symbol-more id="more-symbol"></uui-symbol-more>
</uui-button>
<uui-popover-container id="property-action-popover">
<umb-popover-layout>
<div id="dropdown">${repeat(this._actions, (action) => action.component)}</div>
${repeat(
this._actions,
(action) => action.alias,
(action) => action.component,
)}
</umb-popover-layout>
</uui-popover-container>
`

View File

@@ -0,0 +1,18 @@
import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: UmbBackofficeManifestKind = {
type: 'kind',
alias: 'Umb.Kind.PropertyAction.Default',
matchKind: 'default',
matchType: 'propertyAction',
manifest: {
type: 'propertyAction',
kind: 'default',
weight: 1000,
element: () => import('./property-action.element.js'),
meta: {
icon: 'icon-bug',
label: '(Missing label in manifest)',
},
},
};

View File

@@ -0,0 +1,3 @@
import { manifest as defaultKindManifest } from './default.action.kind.js';
export const manifests = [defaultKindManifest];

View File

@@ -0,0 +1,70 @@
import type { UmbPropertyAction } from '../index.js';
import { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event';
import { html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type {
ManifestPropertyActionDefaultKind,
MetaPropertyActionDefaultKind,
} from '@umbraco-cms/backoffice/extension-registry';
import type { UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-property-action')
export class UmbPropertyActionElement<
MetaType extends MetaPropertyActionDefaultKind = MetaPropertyActionDefaultKind,
ApiType extends UmbPropertyAction<MetaType> = UmbPropertyAction<MetaType>,
> extends UmbLitElement {
#api?: ApiType;
@state()
_href?: string;
@property({ attribute: false })
public manifest?: ManifestPropertyActionDefaultKind<MetaType>;
@property({ attribute: false })
public set api(api: ApiType | undefined) {
this.#api = api;
// TODO: Fix so when we use a HREF it does not refresh the page?
this.#api?.getHref?.().then((href) => {
this._href = href;
// TODO: Do we need to update the component here? [NL]
});
}
async #onClickLabel(event: UUIMenuItemEvent) {
if (!this._href) {
event.stopPropagation();
await this.#api?.execute();
}
this.dispatchEvent(new UmbActionExecutedEvent());
}
// TODO: we need to stop the regular click event from bubbling up to the table so it doesn't select the row.
// This should probably be handled in the UUI Menu item component. so we don't dispatch a label-click event and click event at the same time.
#onClick(event: PointerEvent) {
event.stopPropagation();
}
render() {
return html`
<uui-menu-item
label=${ifDefined(this.manifest?.meta.label)}
href=${ifDefined(this._href)}
@click-label=${this.#onClickLabel}
@click=${this.#onClick}>
${this.manifest?.meta.icon
? html`<umb-icon slot="icon" name="${this.manifest?.meta.icon}"></umb-icon>`
: nothing}
</uui-menu-item>
`;
}
}
export default UmbPropertyActionElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-action': UmbPropertyActionElement;
}
}

View File

@@ -0,0 +1,3 @@
import { manifests as defaultWorkspaceActionManifests } from './default/manifests.js';
export const manifests = [...defaultWorkspaceActionManifests];

View File

@@ -0,0 +1,35 @@
import type { UmbPropertyActionArgs } from './types.js';
import type { UmbPropertyAction } from './property-action.interface.js';
import { UmbActionBase } from '@umbraco-cms/backoffice/action';
/**
* Base class for an property action.
* @export
* @abstract
* @class UmbPropertyActionBase
* @extends {UmbActionBase}
* @implements {UmbPropertyAction}
*/
export abstract class UmbPropertyActionBase<ArgsMetaType = never>
extends UmbActionBase<UmbPropertyActionArgs<ArgsMetaType>>
implements UmbPropertyAction<ArgsMetaType>
{
/**
* By specifying the href, the action will act as a link.
* The `execute` method will not be called.
* @abstract
* @returns {string | undefined}
*/
public getHref(): Promise<string | undefined> {
return Promise.resolve(undefined);
}
/**
* By specifying the `execute` method, the action will act as a button.
* @abstract
* @returns {Promise<void>}
*/
public execute(): Promise<void> {
return Promise.resolve();
}
}

View File

@@ -0,0 +1,16 @@
import type { UmbPropertyActionArgs } from './types.js';
import type { UmbAction } from '@umbraco-cms/backoffice/action';
export interface UmbPropertyAction<ArgsMetaType = never> extends UmbAction<UmbPropertyActionArgs<ArgsMetaType>> {
/**
* The href location, the action will act as a link.
* @returns {Promise<string | undefined>}
*/
getHref(): Promise<string | undefined>;
/**
* The `execute` method, the action will act as a button.
* @returns {Promise<void>}
*/
execute(): Promise<void>;
}

View File

@@ -0,0 +1,3 @@
export interface UmbPropertyActionArgs<MetaArgsType> {
meta: MetaArgsType;
}

View File

@@ -1 +1 @@
export * from './shared/index.js';
export * from './components/index.js';

View File

@@ -1,22 +1,31 @@
import type { ManifestPropertyAction } from '@umbraco-cms/backoffice/extension-registry';
import { manifests as defaultManifests } from './components/property-action/manifests.js';
import type { ManifestPropertyActions } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestPropertyAction> = [
export const propertyActionManifests: Array<ManifestPropertyActions> = [
{
type: 'propertyAction',
kind: 'default',
alias: 'Umb.PropertyAction.Copy',
name: 'Copy Property Action',
js: () => import('./common/copy/property-action-copy.element.js'),
api: () => import('./common/copy/property-action-copy.controller.js'),
forPropertyEditorUis: ['Umb.PropertyEditorUi.TextBox'],
meta: {
propertyEditors: ['Umb.PropertyEditorUi.TextBox'],
icon: 'icon-paste-in',
label: 'Copy',
},
},
{
type: 'propertyAction',
kind: 'default',
alias: 'Umb.PropertyAction.Clear',
name: 'Clear Property Action',
js: () => import('./common/clear/property-action-clear.element.js'),
api: () => import('./common/clear/property-action-clear.controller.js'),
forPropertyEditorUis: ['Umb.PropertyEditorUi.TextBox'],
meta: {
propertyEditors: ['Umb.PropertyEditorUi.TextBox'],
icon: 'icon-trash',
label: 'Clear',
},
},
];
export const manifests = [...defaultManifests, ...propertyActionManifests];

View File

@@ -1,3 +0,0 @@
export interface UmbPropertyAction extends HTMLElement {
value?: unknown;
}

View File

@@ -156,6 +156,9 @@ export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
}
public resetValue(): void {
this.setValue(undefined); // TODO: We should get the value from the server aka. the value from the persisted data.
}
public clearValue(): void {
this.setValue(undefined); // TODO: We should get the default value from Property Editor maybe even later the DocumentType, as that would hold the default value for the property.
}

View File

@@ -220,8 +220,7 @@ export class UmbPropertyElement extends UmbLitElement {
? html`<umb-property-action-menu
slot="action-menu"
id="action-menu"
.propertyEditorUiAlias=${this._propertyEditorUiAlias}
.value=${this._value}></umb-property-action-menu>`
.propertyEditorUiAlias=${this._propertyEditorUiAlias}></umb-property-action-menu>`
: ''}`;
}
@@ -238,6 +237,7 @@ export class UmbPropertyElement extends UmbLitElement {
#action-menu {
opacity: 0;
transition: opacity 90ms;
}
#layout:focus-within #action-menu,

View File

@@ -2,7 +2,7 @@ import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extensio
export const manifest: UmbBackofficeManifestKind = {
type: 'kind',
alias: 'Umb.Kind.WorkspaceAction.Default',
alias: 'Umb.Kind.WorkspaceActionMenuItem.Default',
matchKind: 'default',
matchType: 'workspaceActionMenuItem',
manifest: {

View File

@@ -86,11 +86,6 @@ export class UmbWorkspaceActionMenuElement extends UmbLitElement {
this._popoverOpen = event.newState === 'open';
}
#onActionExecuted(event: MouseEvent) {
// TODO: Explicit close the popover?
// Should we stop the event as well?
}
render() {
return this._items && this._items.length > 0
? html`
@@ -105,7 +100,7 @@ export class UmbWorkspaceActionMenuElement extends UmbLitElement {
</uui-button>
<uui-popover-container
id="workspace-action-popover"
margin="5"
margin="6"
placement="top-end"
@toggle=${this.#onPopoverToggle}>
<umb-popover-layout>

View File

@@ -1,5 +1,3 @@
export interface UmbWorkspaceActionArgs<MetaArgsType> {
entityType: string;
unique: string | null;
meta: MetaArgsType;
}

View File

@@ -6,8 +6,11 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbModalContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { ManifestWorkspaceAction, MetaWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbWorkspaceActionArgs } from '@umbraco-cms/backoffice/workspace';
function ExtensionApiArgsMethod(manifest: ManifestWorkspaceAction<MetaWorkspaceAction>) {
function ExtensionApiArgsMethod(
manifest: ManifestWorkspaceAction<MetaWorkspaceAction>,
): [UmbWorkspaceActionArgs<MetaWorkspaceAction>] {
return [{ meta: manifest.meta }];
}