Feature: workspace action additional options ellipsis (#18299)

* adding types and default kind element

* switch to use ?? for code consistency

* add data-mark

* move listener last

* fix tsc

* save action additionalOptions

* hasAdditionalOptions for Save and Publish

* remove unused import

* type arg

* rename to _retrieveWorkspaceContext
This commit is contained in:
Niels Lyngsø
2025-02-12 10:16:12 +01:00
committed by GitHub
parent f906d28de6
commit 3d8400302a
18 changed files with 164 additions and 93 deletions

View File

@@ -0,0 +1,2 @@
export type * from './workspace-action/types.js';
export type * from './workspace-action-menu-item/types.js';

View File

@@ -1,4 +1,4 @@
import type { UmbWorkspaceActionMenuItem } from '../index.js';
import type { UmbWorkspaceActionMenuItem } from '../types.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';

View File

@@ -1,3 +1 @@
export * from './workspace-action-menu-item-base.controller.js';
export type * from './types.js';
export type * from './workspace-action-menu-item.interface.js';

View File

@@ -1,3 +1,4 @@
export type * from './workspace-action-menu-item.interface.js';
export interface UmbWorkspaceActionMenuItemArgs<MetaArgsType> {
meta: MetaArgsType;
}

View File

@@ -1 +1,2 @@
export * from './submit.action.js';
export type * from './types.js';

View File

@@ -1,30 +1,40 @@
import type { MetaWorkspaceAction } from '../../../../types.js';
import { UMB_SUBMITTABLE_WORKSPACE_CONTEXT } from '../../../../contexts/tokens/index.js';
import type { UmbSubmittableWorkspaceContext } from '../../../../contexts/tokens/index.js';
import type { UmbWorkspaceActionArgs } from '../../types.js';
import { UmbWorkspaceActionBase } from '../../workspace-action-base.controller.js';
import type { UmbSubmitWorkspaceActionArgs } from './types.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbSubmitWorkspaceAction extends UmbWorkspaceActionBase<UmbSubmittableWorkspaceContext> {
#workspaceContext?: UmbSubmittableWorkspaceContext;
export class UmbSubmitWorkspaceAction<
ArgsMetaType extends MetaWorkspaceAction = MetaWorkspaceAction,
WorkspaceContextType extends UmbSubmittableWorkspaceContext = UmbSubmittableWorkspaceContext,
> extends UmbWorkspaceActionBase<ArgsMetaType> {
protected _retrieveWorkspaceContext: Promise<unknown>;
protected _workspaceContext?: WorkspaceContextType;
constructor(host: UmbControllerHost, args: UmbWorkspaceActionArgs<UmbSubmittableWorkspaceContext>) {
constructor(host: UmbControllerHost, args: UmbSubmitWorkspaceActionArgs<ArgsMetaType>) {
super(host, args);
// TODO: Could we make change label depending on the state?
this.consumeContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT, (context) => {
this.#workspaceContext = context;
this.#observeUnique();
});
// TODO: Could we make change label depending on the state? [NL]
this._retrieveWorkspaceContext = this.consumeContext(
args.workspaceContextToken ?? UMB_SUBMITTABLE_WORKSPACE_CONTEXT,
(context) => {
this._workspaceContext = context as WorkspaceContextType;
this.#observeUnique();
this._gotWorkspaceContext();
},
).asPromise();
}
#observeUnique() {
this.observe(
this.#workspaceContext?.unique,
this._workspaceContext?.unique,
(unique) => {
// We can't save if we don't have a unique
if (unique === undefined) {
this.disable();
} else {
// Dangerous, cause this could enable despite a class extension decided to disable it?. [NL]
this.enable();
}
},
@@ -32,9 +42,13 @@ export class UmbSubmitWorkspaceAction extends UmbWorkspaceActionBase<UmbSubmitta
);
}
protected _gotWorkspaceContext() {
// Override in subclass
}
override async execute() {
const workspaceContext = await this.getContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT);
return await workspaceContext.requestSubmit();
await this._retrieveWorkspaceContext;
return await this._workspaceContext!.requestSubmit();
}
}

View File

@@ -0,0 +1,7 @@
import type { UmbWorkspaceActionArgs } from '../../types.js';
import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
export interface UmbSubmitWorkspaceActionArgs<MetaArgsType> extends UmbWorkspaceActionArgs<MetaArgsType> {
workspaceContextToken?: string | UmbContextToken<UmbSubmittableWorkspaceContext, UmbSubmittableWorkspaceContext>;
}

View File

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

View File

@@ -1,4 +1,19 @@
import { manifest as defaultKindManifest } from './default.action.kind.js';
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [defaultKindManifest];
export const manifest: UmbExtensionManifestKind = {
type: 'kind',
alias: 'Umb.Kind.WorkspaceAction.Default',
matchKind: 'default',
matchType: 'workspaceAction',
manifest: {
type: 'workspaceAction',
kind: 'default',
weight: 1000,
element: () => import('./workspace-action-default-kind.element.js'),
meta: {
label: '(Missing label in manifest)',
},
},
};
export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [manifest];

View File

@@ -1,11 +1,11 @@
import type { UmbWorkspaceAction } from '../workspace-action.interface.js';
import type {
ManifestWorkspaceAction,
ManifestWorkspaceActionMenuItem,
MetaWorkspaceActionDefaultKind,
UmbWorkspaceActionDefaultKind,
} from '../../../types.js';
import { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event';
import { html, customElement, property, state, ifDefined, when } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, property, state, when } from '@umbraco-cms/backoffice/external/lit';
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -19,7 +19,7 @@ import '../../workspace-action-menu/index.js';
@customElement('umb-workspace-action')
export class UmbWorkspaceActionElement<
MetaType extends MetaWorkspaceActionDefaultKind = MetaWorkspaceActionDefaultKind,
ApiType extends UmbWorkspaceAction<MetaType> = UmbWorkspaceAction<MetaType>,
ApiType extends UmbWorkspaceActionDefaultKind<MetaType> = UmbWorkspaceActionDefaultKind<MetaType>,
> extends UmbLitElement {
#manifest?: ManifestWorkspaceAction<MetaType>;
#api?: ApiType;
@@ -29,21 +29,14 @@ export class UmbWorkspaceActionElement<
ManifestWorkspaceActionMenuItem
>;
@state()
private _buttonState?: UUIButtonState;
@state()
_href?: string;
@state()
_isDisabled = false;
@property({ type: Object, attribute: false })
public set manifest(value: ManifestWorkspaceAction<MetaType> | undefined) {
if (!value) return;
const oldValue = this.#manifest;
this.#manifest = value;
if (oldValue !== this.#manifest) {
if (oldValue !== value) {
this.#manifest = value;
this._href = value?.meta.href;
this._additionalOptions = value?.meta.additionalOptions;
this.#createAliases();
this.requestUpdate('manifest', oldValue);
}
@@ -56,10 +49,12 @@ export class UmbWorkspaceActionElement<
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]
this._href = href ?? this.manifest?.meta.href;
});
this.#api?.hasAdditionalOptions?.().then((additionalOptions) => {
this._additionalOptions = additionalOptions ?? this.manifest?.meta.additionalOptions;
});
this.#observeIsDisabled();
@@ -68,6 +63,18 @@ export class UmbWorkspaceActionElement<
return this.#api;
}
@state()
private _buttonState?: UUIButtonState;
@state()
private _additionalOptions?: boolean;
@state()
private _href?: string;
@state()
_isDisabled = false;
@state()
private _items: Array<UmbExtensionElementAndApiInitializer<ManifestWorkspaceActionMenuItem>> = [];
@@ -92,21 +99,28 @@ export class UmbWorkspaceActionElement<
this.#observeExtensions(Array.from(aliases));
}
private async _onClick(event: MouseEvent) {
async #onClick(event: MouseEvent) {
if (this._href) {
event.stopPropagation();
}
// If its a link or has additional options, then we do not want to display state on the button. [NL]
if (!this._href) {
if (!this._additionalOptions) {
this._buttonState = 'waiting';
}
this._buttonState = 'waiting';
try {
if (!this.#api) throw new Error('No api defined');
await this.#api.execute();
this._buttonState = 'success';
} catch {
this._buttonState = 'failed';
try {
if (!this.#api) throw new Error('No api defined');
await this.#api.execute();
if (!this._additionalOptions) {
this._buttonState = 'success';
}
} catch {
if (!this._additionalOptions) {
this._buttonState = 'failed';
}
}
}
this.dispatchEvent(new UmbActionExecutedEvent());
}
@@ -144,18 +158,19 @@ export class UmbWorkspaceActionElement<
}
#renderButton() {
const label = this.#manifest?.meta.label
? this.localize.string(this.#manifest.meta.label)
: (this.#manifest?.name ?? '');
return html`
<uui-button
id="action-button"
data-mark="workspace-action:${this.#manifest?.alias}"
.href=${this._href}
@click=${this._onClick}
look=${this.#manifest?.meta.look || 'default'}
color=${this.#manifest?.meta.color || 'default'}
label=${ifDefined(
this.#manifest?.meta.label ? this.localize.string(this.#manifest.meta.label) : this.#manifest?.name,
)}
look=${this.#manifest?.meta.look ?? 'default'}
color=${this.#manifest?.meta.color ?? 'default'}
label=${this._additionalOptions ? label + '…' : label}
.disabled=${this._isDisabled}
.state=${this._buttonState}></uui-button>
.state=${this._buttonState}
@click=${this.#onClick}></uui-button>
`;
}
@@ -163,8 +178,8 @@ export class UmbWorkspaceActionElement<
return html`
<umb-workspace-action-menu
.items=${this._items}
color="${this.#manifest?.meta.color || 'default'}"
look="${this.#manifest?.meta.look || 'default'}"></umb-workspace-action-menu>
color="${this.#manifest?.meta.color ?? 'default'}"
look="${this.#manifest?.meta.look ?? 'default'}"></umb-workspace-action-menu>
`;
}

View File

@@ -0,0 +1,9 @@
import type { UmbWorkspaceAction } from '../types.js';
export interface UmbWorkspaceActionDefaultKind<ArgsMetaType = never> extends UmbWorkspaceAction<ArgsMetaType> {
/**
* The action has additional options.
* @returns {undefined | Promise<boolean | undefined>}
*/
hasAdditionalOptions?(): Promise<boolean | undefined>;
}

View File

@@ -1,4 +1,2 @@
export * from './common/index.js';
export * from './workspace-action-base.controller.js';
export type * from './types.js';
export type * from './workspace-action.interface.js';

View File

@@ -1,3 +1,6 @@
export type * from './workspace-action.interface.js';
export type * from './default/workspace-action-default-kind.interface.js';
export interface UmbWorkspaceActionArgs<MetaArgsType> {
meta: MetaArgsType;
}

View File

@@ -9,7 +9,7 @@ export interface UmbWorkspaceAction<ArgsMetaType = never> extends UmbAction<UmbW
* The href location, the action will act as a link.
* @returns {Promise<string | undefined>}
*/
getHref(): Promise<string | undefined>;
getHref?(): Promise<string | undefined>;
/**
* The `execute` method, the action will act as a button.

View File

@@ -1,10 +1,12 @@
import type { UmbWorkspaceAction, UmbWorkspaceActionDefaultKind } from '../types.js';
import type { UUIInterfaceColor, UUIInterfaceLook } from '@umbraco-cms/backoffice/external/uui';
import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
import type { UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export interface ManifestWorkspaceAction<MetaType extends MetaWorkspaceAction = MetaWorkspaceAction>
extends ManifestElementAndApi<UmbControllerHostElement, UmbWorkspaceAction<MetaType>>,
export interface ManifestWorkspaceAction<
MetaType extends MetaWorkspaceAction = MetaWorkspaceAction,
ApiType extends UmbWorkspaceAction<MetaType> = UmbWorkspaceAction<MetaType>,
> extends ManifestElementAndApi<UmbControllerHostElement, ApiType>,
ManifestWithDynamicConditions<UmbExtensionConditionConfig> {
type: 'workspaceAction';
meta: MetaType;
@@ -13,7 +15,8 @@ export interface ManifestWorkspaceAction<MetaType extends MetaWorkspaceAction =
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface MetaWorkspaceAction {}
export interface ManifestWorkspaceActionDefaultKind extends ManifestWorkspaceAction<MetaWorkspaceActionDefaultKind> {
export interface ManifestWorkspaceActionDefaultKind
extends ManifestWorkspaceAction<MetaWorkspaceActionDefaultKind, UmbWorkspaceActionDefaultKind> {
type: 'workspaceAction';
kind: 'default';
}
@@ -22,6 +25,8 @@ export interface MetaWorkspaceActionDefaultKind extends MetaWorkspaceAction {
label?: string;
look?: UUIInterfaceLook;
color?: UUIInterfaceColor;
href?: string;
additionalOptions?: boolean;
}
declare global {

View File

@@ -1,5 +1,6 @@
import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
export type * from './components/types.js';
export type * from './conditions/types.js';
export type * from './data-manager/types.js';
export type * from './extensions/types.js';

View File

@@ -4,11 +4,12 @@ import {
UMB_USER_PERMISSION_DOCUMENT_UPDATE,
} from '../../../user-permissions/constants.js';
import { UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT } from '../../workspace-context/constants.js';
import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace';
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../../constants.js';
import { UmbWorkspaceActionBase, type UmbWorkspaceActionArgs } from '@umbraco-cms/backoffice/workspace';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbDocumentSaveAndPublishWorkspaceAction extends UmbWorkspaceActionBase {
constructor(host: UmbControllerHost, args: any) {
constructor(host: UmbControllerHost, args: UmbWorkspaceActionArgs<never>) {
super(host, args);
/* The action is disabled by default because the onChange callback
@@ -31,6 +32,12 @@ export class UmbDocumentSaveAndPublishWorkspaceAction extends UmbWorkspaceAction
});
}
async hasAdditionalOptions() {
const workspaceContext = await this.getContext(UMB_DOCUMENT_WORKSPACE_CONTEXT);
const variantOptions = await this.observe(workspaceContext.variantOptions).asPromise();
return variantOptions?.length > 1;
}
override async execute() {
const workspaceContext = await this.getContext(UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT);
return workspaceContext.saveAndPublish();

View File

@@ -3,27 +3,40 @@ import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../document-workspace.context-to
import type UmbDocumentWorkspaceContext from '../document-workspace.context.js';
import type { UmbVariantState } from '@umbraco-cms/backoffice/utils';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbSubmitWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import {
UmbSubmitWorkspaceAction,
type MetaWorkspaceAction,
type UmbSubmitWorkspaceActionArgs,
type UmbWorkspaceActionDefaultKind,
} from '@umbraco-cms/backoffice/workspace';
export class UmbDocumentSaveWorkspaceAction extends UmbSubmitWorkspaceAction {
#documentWorkspaceContext?: UmbDocumentWorkspaceContext;
export class UmbDocumentSaveWorkspaceAction
extends UmbSubmitWorkspaceAction<MetaWorkspaceAction, UmbDocumentWorkspaceContext>
implements UmbWorkspaceActionDefaultKind<MetaWorkspaceAction>
{
#variants: Array<UmbDocumentVariantModel> = [];
#readOnlyStates: Array<UmbVariantState> = [];
constructor(host: UmbControllerHost, args: any) {
super(host, args);
constructor(host: UmbControllerHost, args: UmbSubmitWorkspaceActionArgs<MetaWorkspaceAction>) {
super(host, { workspaceContextToken: UMB_DOCUMENT_WORKSPACE_CONTEXT, ...args });
}
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => {
this.#documentWorkspaceContext = context;
this.#observeVariants();
this.#observeReadOnlyStates();
});
async hasAdditionalOptions() {
await this._retrieveWorkspaceContext;
const variantOptions = await this.observe(this._workspaceContext!.variantOptions).asPromise();
return variantOptions?.length > 1;
}
override _gotWorkspaceContext() {
super._gotWorkspaceContext();
this.#observeVariants();
this.#observeReadOnlyStates();
}
#observeVariants() {
this.observe(
this.#documentWorkspaceContext?.variants,
this._workspaceContext?.variants,
(variants) => {
this.#variants = variants ?? [];
this.#check();
@@ -34,7 +47,7 @@ export class UmbDocumentSaveWorkspaceAction extends UmbSubmitWorkspaceAction {
#observeReadOnlyStates() {
this.observe(
this.#documentWorkspaceContext?.readOnlyState.states,
this._workspaceContext?.readOnlyState.states,
(readOnlyStates) => {
this.#readOnlyStates = readOnlyStates ?? [];
this.#check();