Implement Can Start Workflow context menu
This commit is contained in:
parent
ca19adf704
commit
44859f3c16
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
;
|
||||
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface Activity {
|
|||
id: string;
|
||||
typeName: string;
|
||||
metadata: any;
|
||||
canStartWorkflow?: boolean;
|
||||
applicationProperties: any;
|
||||
|
||||
[name: string]: any;
|
||||
|
|
|
|||
Loading…
Reference in a new issue