Implement Can Start Workflow context menu

This commit is contained in:
Sipke Schoorstra 2022-04-27 19:49:11 +02:00
parent ca19adf704
commit 44859f3c16
6 changed files with 149 additions and 23 deletions

View file

@ -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<void>;
"hideButton": boolean;
"menuItems": Array<MenuItem>;
"open": () => Promise<void>;
}
interface ElsaDropdownButton {
"icon"?: any;
@ -429,6 +433,8 @@ declare namespace LocalJSX {
"inputContext"?: ActivityInputContext;
}
interface ElsaContextMenu {
"anchorPoint"?: ContextMenuAnchorPoint;
"hideButton"?: boolean;
"menuItems"?: Array<MenuItem>;
}
interface ElsaDropdownButton {

View file

@ -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 (
<div
class="absolute left-0 top-0 right-0 bottom-0"
ref={el => this.container = el}/>
<div class="relative">
<div
class="absolute left-0 top-0 right-0 bottom-0"
ref={el => this.container = el}>
</div>
<elsa-context-menu ref={el => this.activityContextMenu = el}
hideButton={true}
anchorPoint={ContextMenuAnchorPoint.TopLeft}
class="absolute"/>
</div>
);
}
private onEditMenuClicked: (e: MouseEvent) => void;
public disableEvents = () => this.silent = true;
public enableEvents = async (emitWorkflowChanged: boolean): Promise<void> => {
@ -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<JQuery.ContextMenuEvent>) => {
const activity = e.node.data as Activity;
const canStartWorkflow = activity.canStartWorkflow;
const menuItems: Array<MenuItem> = [{
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<JQuery.ClickEvent>) => {
const {node, x, y} = e;
const activity = node.data as Activity;

View file

@ -0,0 +1,7 @@
import {FunctionalComponent, h} from "@stencil/core";
export const TickIcon: FunctionalComponent = () =>
<svg class="h-5 w-5 text-blue-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
;

View file

@ -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<MenuItem> = [];
@Prop({mutable: true}) menuItems: Array<MenuItem> = [];
@Prop() hideButton: boolean;
@Prop() anchorPoint: ContextMenuAnchorPoint;
contextMenu: HTMLElement;
element: HTMLElement;
@Method()
async open(): Promise<void> {
this.showContextMenu();
}
@Method()
async close(): Promise<void> {
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 (
<div class="relative flex justify-end items-center" ref={el => this.element = el}>
<button onClick={() => this.toggleMenu()} aria-has-popup="true" type="button"
class="w-8 h-8 inline-flex items-center justify-center text-gray-400 rounded-full bg-transparent hover:text-gray-500 focus:outline-none focus:text-gray-500 focus:bg-gray-100 transition ease-in-out duration-150">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
</svg>
</button>
{this.renderButton()}
<div ref={el => 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`}>
<div class="rounded-md bg-white shadow-xs" role="menu" aria-orientation="vertical" aria-labelledby="project-options-menu-0">
{this.menuItems.map(menuItem => {
{menuItems.map(menuItem => {
const anchorUrl = menuItem.anchorUrl || '#';
const isToggle = menuItem.isToggle;
const checked = menuItem.checked;
return (
<div class="py-1">
<a href={anchorUrl}
onClick={e => this.onMenuItemClick(e, menuItem)}
class="group flex items-center px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
role="menuitem">
{menuItem.icon ? <span class="mr-3 ">{menuItem.icon}</span> : undefined}
{menuItem.text}
{menuItem.icon ? <span class="mr-3">{menuItem.icon}</span> : hasAnyIcons ? <span class="mr-7"/> : undefined}
<span class="flex-grow">{menuItem.text}</span>
{isToggle && checked ? <span class="float-right"><TickIcon/></span> : undefined}
</a>
</div>
);
@ -74,4 +115,16 @@ export class ContextMenu {
</div>
);
}
renderButton = () => {
if (this.hideButton)
return;
return <button onClick={() => this.toggleMenu()} aria-has-popup="true" type="button"
class="w-8 h-8 inline-flex items-center justify-center text-gray-400 rounded-full bg-transparent hover:text-gray-500 focus:outline-none focus:text-gray-500 focus:bg-gray-100 transition ease-in-out duration-150">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
</svg>
</button>;
}
}

View file

@ -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
}

View file

@ -6,6 +6,7 @@ export interface Activity {
id: string;
typeName: string;
metadata: any;
canStartWorkflow?: boolean;
applicationProperties: any;
[name: string]: any;