From 44859f3c166b2a246b5245cc9279c561e374abbb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 27 Apr 2022 19:49:11 +0200 Subject: [PATCH] Implement Can Start Workflow context menu --- .../src/components.d.ts | 8 +- .../activities/flowchart/flowchart.tsx | 57 +++++++++++- .../src/components/icons/tooling/tick.tsx | 7 ++ .../shared/context-menu/context-menu.tsx | 89 +++++++++++++++---- .../components/shared/context-menu/models.tsx | 10 +++ .../src/models/core.ts | 1 + 6 files changed, 149 insertions(+), 23 deletions(-) create mode 100644 src/designer/elsa-workflows-designer/src/components/icons/tooling/tick.tsx diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index 5910d9df7..c4bbc197d 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -10,7 +10,7 @@ import { ActivityUpdatedArgs, DeleteActivityRequestedArgs } from "./components/d import { ContainerActivityComponent } from "./components/activities/container-activity-component"; import { AddActivityArgs } from "./components/designer/canvas/canvas"; import { ActivityInputContext } from "./services/node-input-driver"; -import { MenuItem } from "./components/shared/context-menu/models"; +import { ContextMenuAnchorPoint, MenuItem } from "./components/shared/context-menu/models"; import { DropdownButtonItem, DropdownButtonOrigin } from "./components/shared/dropdown-button/models"; import { Graph } from "@antv/x6"; import { AddActivityArgs as AddActivityArgs1 } from "./components/designer/canvas/canvas"; @@ -44,7 +44,11 @@ export namespace Components { "inputContext": ActivityInputContext; } interface ElsaContextMenu { + "anchorPoint": ContextMenuAnchorPoint; + "close": () => Promise; + "hideButton": boolean; "menuItems": Array; + "open": () => Promise; } interface ElsaDropdownButton { "icon"?: any; @@ -429,6 +433,8 @@ declare namespace LocalJSX { "inputContext"?: ActivityInputContext; } interface ElsaContextMenu { + "anchorPoint"?: ContextMenuAnchorPoint; + "hideButton"?: boolean; "menuItems"?: Array; } interface ElsaDropdownButton { diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx index a468f19a3..c0032a522 100644 --- a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/flowchart.tsx @@ -19,6 +19,7 @@ import {ConnectionCreatedEventArgs, FlowchartEvents} from "./events"; import {TransposeHandlerRegistry} from "./transpose-handler-registry"; import PositionEventArgs = NodeView.PositionEventArgs; import FromJSONData = Model.FromJSONData; +import {ContextMenuAnchorPoint, MenuItem} from "../../shared/context-menu/models"; @Component({ tag: 'elsa-flowchart', @@ -29,6 +30,7 @@ export class FlowchartComponent implements ContainerActivityComponent { private readonly nodeFactory: NodeFactory; private rootId: string = uuid(); private silent: boolean = false; // Whether to emit events or not. + private activityContextMenu: HTMLElsaContextMenuElement; constructor() { this.eventBus = Container.get(EventBus); @@ -99,13 +101,23 @@ export class FlowchartComponent implements ContainerActivityComponent { } public render() { + return ( -
this.container = el}/> +
+
this.container = el}> +
+ this.activityContextMenu = el} + hideButton={true} + anchorPoint={ContextMenuAnchorPoint.TopLeft} + class="absolute"/> +
); } + private onEditMenuClicked: (e: MouseEvent) => void; + public disableEvents = () => this.silent = true; public enableEvents = async (emitWorkflowChanged: boolean): Promise => { @@ -135,6 +147,7 @@ export class FlowchartComponent implements ContainerActivityComponent { graph.on('blank:click', this.onGraphClick); graph.on('node:click', this.onNodeClick); + graph.on('node:contextmenu', this.onNodeContextMenu); graph.on('edge:connected', this.onEdgeConnected); graph.on('node:moved', this.onNodeMoved); @@ -175,12 +188,21 @@ export class FlowchartComponent implements ContainerActivityComponent { } } + debugger; + let rootActivity = activities.find(activity => { + const hasInboundConnections = connections.find(c => c.target == activity.id) != null; + return !hasInboundConnections; + }); + + if (!rootActivity) + rootActivity = first(activities); + return { typeName: 'Elsa.Flowchart', activities: remainingActivities, connections: remainingConnections, id: this.rootId, - start: first(activities)?.id, + start: rootActivity?.id, metadata: {}, applicationProperties: {}, variables: [] @@ -333,6 +355,33 @@ export class FlowchartComponent implements ContainerActivityComponent { this.activitySelected.emit(args); }; + private onNodeContextMenu = async (e: PositionEventArgs) => { + const activity = e.node.data as Activity; + const canStartWorkflow = activity.canStartWorkflow; + + const menuItems: Array = [{ + text: 'Startable', + clickHandler: () => { + activity.canStartWorkflow = !activity.canStartWorkflow; + this.onGraphChanged(); + }, + isToggle: true, + checked: canStartWorkflow, + }, { + text: 'Edit', + clickHandler: this.onEditMenuClicked + }, { + text: 'Delete', + clickHandler: this.onEditMenuClicked + }]; + + this.activityContextMenu.menuItems = menuItems; + this.activityContextMenu.style.top = `${e.y}px`; + this.activityContextMenu.style.left = `${e.x}px`; + + await this.activityContextMenu.open(); + } + private onNodeMoved = (e: PositionEventArgs) => { const {node, x, y} = e; const activity = node.data as Activity; diff --git a/src/designer/elsa-workflows-designer/src/components/icons/tooling/tick.tsx b/src/designer/elsa-workflows-designer/src/components/icons/tooling/tick.tsx new file mode 100644 index 000000000..18a2d46a8 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/icons/tooling/tick.tsx @@ -0,0 +1,7 @@ +import {FunctionalComponent, h} from "@stencil/core"; + +export const TickIcon: FunctionalComponent = () => + + + +; diff --git a/src/designer/elsa-workflows-designer/src/components/shared/context-menu/context-menu.tsx b/src/designer/elsa-workflows-designer/src/components/shared/context-menu/context-menu.tsx index 965552724..2ae1bff2a 100644 --- a/src/designer/elsa-workflows-designer/src/components/shared/context-menu/context-menu.tsx +++ b/src/designer/elsa-workflows-designer/src/components/shared/context-menu/context-menu.tsx @@ -1,70 +1,111 @@ -import {Component, h, Listen, Prop} from '@stencil/core'; -import {leave, toggle} from 'el-transition' -import {MenuItem} from "./models"; +import {Component, h, Listen, Method, Prop} from '@stencil/core'; +import {leave, toggle, enter} from 'el-transition' +import {ContextMenuAnchorPoint, MenuItem} from "./models"; +import {TickIcon} from "../../icons/tooling/tick"; @Component({ tag: 'elsa-context-menu', shadow: false, }) export class ContextMenu { - @Prop() menuItems: Array = []; + @Prop({mutable: true}) menuItems: Array = []; + @Prop() hideButton: boolean; + @Prop() anchorPoint: ContextMenuAnchorPoint; contextMenu: HTMLElement; element: HTMLElement; + @Method() + async open(): Promise { + this.showContextMenu(); + } + + @Method() + async close(): Promise { + this.closeContextMenu(); + } + @Listen('click', {target: 'window'}) - onWindowClicked(event: Event){ + private onWindowClicked(event: Event) { const target = event.target as HTMLElement; if (!this.element.contains(target)) this.closeContextMenu(); } - closeContextMenu() { + private showContextMenu() { + if (!!this.contextMenu) + enter(this.contextMenu); + } + + private closeContextMenu() { if (!!this.contextMenu) leave(this.contextMenu); } - toggleMenu() { + private toggleMenu() { toggle(this.contextMenu); } - async onMenuItemClick(e: MouseEvent, menuItem: MenuItem) { + private async onMenuItemClick(e: MouseEvent, menuItem: MenuItem) { + debugger; e.preventDefault(); if (!!menuItem.clickHandler) menuItem.clickHandler(e); + if (menuItem.isToggle) { + menuItem.checked = !menuItem.checked; + this.menuItems = [...this.menuItems]; // Trigger a re-render. + } + this.closeContextMenu(); } + private getAnchorPointClass = (): string => { + switch (this.anchorPoint) { + case ContextMenuAnchorPoint.BottomLeft: + return 'origin-bottom-left'; + case ContextMenuAnchorPoint.BottomRight: + return 'origin-bottom-right'; + case ContextMenuAnchorPoint.TopLeft: + return 'origin-top-left'; + case ContextMenuAnchorPoint.TopRight: + default: + return 'origin-top-right' + } + }; + render() { + + const anchorPointClass = this.getAnchorPointClass(); + const menuItems = this.menuItems; + const hasAnyIcons = menuItems.find(x => !!x.icon) != null; + return (
this.element = el}> - + {this.renderButton()}
this.contextMenu = el} data-transition-enter="transition ease-out duration-100" data-transition-enter-start="transform opacity-0 scale-95" data-transition-leave="transition ease-in duration-75" data-transition-leave-start="transform opacity-100 scale-100" data-transition-leave-end="transform opacity-0 scale-95" - class="hidden z-10 mx-3 origin-top-right absolute right-7 top-0 w-48 mt-1 rounded-md shadow-lg"> + class={`hidden z-10 mx-3 absolute ${anchorPointClass} w-48 mt-1 rounded-md shadow-lg`}> ); } + + renderButton = () => { + if (this.hideButton) + return; + + return ; + } } diff --git a/src/designer/elsa-workflows-designer/src/components/shared/context-menu/models.tsx b/src/designer/elsa-workflows-designer/src/components/shared/context-menu/models.tsx index 072dc3ed6..0fb8b254e 100644 --- a/src/designer/elsa-workflows-designer/src/components/shared/context-menu/models.tsx +++ b/src/designer/elsa-workflows-designer/src/components/shared/context-menu/models.tsx @@ -3,4 +3,14 @@ anchorUrl?: string; clickHandler?: (e: MouseEvent) => void; icon?: any; + isToggle?: boolean; // Whether this item can be checked/unchecked. + checked?: boolean; +} + + +export enum ContextMenuAnchorPoint { + TopLeft, + TopRight, + BottomLeft, + BottomRight } diff --git a/src/designer/elsa-workflows-designer/src/models/core.ts b/src/designer/elsa-workflows-designer/src/models/core.ts index 842fbcd5b..d5902c452 100644 --- a/src/designer/elsa-workflows-designer/src/models/core.ts +++ b/src/designer/elsa-workflows-designer/src/models/core.ts @@ -6,6 +6,7 @@ export interface Activity { id: string; typeName: string; metadata: any; + canStartWorkflow?: boolean; applicationProperties: any; [name: string]: any;