Implement activity context menu actions

This commit is contained in:
Sipke Schoorstra 2022-04-28 12:37:39 +02:00
parent 4b42098513
commit e12604db04
12 changed files with 134 additions and 67 deletions

View file

@ -52,7 +52,7 @@ public class ActivityDescriber : IActivityDescriber
var properties = activityType.GetProperties();
var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute<InputAttribute>() != null).ToList();
var outputProperties = properties.Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).ToList();
var isTrigger = activityType.IsAssignableTo(typeof(IEventGenerator));
var isTrigger = activityType.IsAssignableTo(typeof(ITrigger));
var descriptor = new ActivityDescriptor
{
@ -60,7 +60,7 @@ public class ActivityDescriber : IActivityDescriber
Description = description,
ActivityType = fullTypeName,
DisplayName = displayName,
Traits = isTrigger ? ActivityTraits.Trigger : ActivityTraits.Action,
Kind = isTrigger ? ActivityKind.Trigger : ActivityKind.Action,
OutPorts = outboundPorts.ToList(),
InputProperties = DescribeInputProperties(inputProperties).ToList(),
OutputProperties = DescribeOutputProperties(outputProperties).ToList(),

View file

@ -14,7 +14,7 @@ public class ActivityDescriptor
public ICollection<OutputDescriptor> OutputProperties { get; init; } = new List<OutputDescriptor>();
[JsonIgnore] public Func<ActivityConstructorContext, IActivity> Constructor { get; init; } = default!;
public ActivityTraits Traits { get; set; } = ActivityTraits.Action;
public ActivityKind Kind { get; set; } = ActivityKind.Action;
public ICollection<Port> InPorts { get; init; } = new List<Port>();
public ICollection<Port> OutPorts { get; init; } = new List<Port>();
}

View file

@ -0,0 +1,7 @@
namespace Elsa.Management.Models;
public enum ActivityKind
{
Action,
Trigger
}

View file

@ -1,8 +0,0 @@
namespace Elsa.Management.Models;
[Flags]
public enum ActivityTraits
{
Action = 1,
Trigger = 2
}

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 { ContextMenuAnchorPoint, MenuItem } from "./components/shared/context-menu/models";
import { ContextMenuAnchorPoint, MenuItem, MenuItemGroup } 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";
@ -47,6 +47,7 @@ export namespace Components {
"anchorPoint": ContextMenuAnchorPoint;
"close": () => Promise<void>;
"hideButton": boolean;
"menuItemGroups": Array<MenuItemGroup>;
"menuItems": Array<MenuItem>;
"open": () => Promise<void>;
}
@ -435,6 +436,7 @@ declare namespace LocalJSX {
interface ElsaContextMenu {
"anchorPoint"?: ContextMenuAnchorPoint;
"hideButton"?: boolean;
"menuItemGroups"?: Array<MenuItemGroup>;
"menuItems"?: Array<MenuItem>;
}
interface ElsaDropdownButton {

View file

@ -20,7 +20,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";
import {ContextMenuAnchorPoint, MenuItem, MenuItemGroup} from "../../shared/context-menu/models";
@Component({
tag: 'elsa-flowchart',
@ -117,8 +117,6 @@ export class FlowchartComponent implements ContainerActivityComponent {
);
}
private onEditMenuClicked: (e: MouseEvent) => void;
public disableEvents = () => this.silent = true;
public enableEvents = async (emitWorkflowChanged: boolean): Promise<void> => {
@ -358,26 +356,33 @@ export class FlowchartComponent implements ContainerActivityComponent {
private onNodeContextMenu = async (e: PositionEventArgs<JQuery.ContextMenuEvent>) => {
const node = e.node as ActivityNodeShape;
const activity = e.node.data as Activity;
const canStartWorkflow = activity.canStartWorkflow;
const menuItems: Array<MenuItem> = [{
text: 'Startable',
clickHandler: () => {
activity.canStartWorkflow = !activity.canStartWorkflow;
node.activity = {...activity};
this.onGraphChanged();
},
isToggle: true,
checked: canStartWorkflow,
}, {
text: 'Edit',
clickHandler: this.onEditMenuClicked
}, {
text: 'Delete',
clickHandler: this.onEditMenuClicked
}];
const menuItemGroups: Array<MenuItemGroup> = [
{
menuItems: [{
text: 'Startable',
clickHandler: () => this.onToggleCanStartWorkflowClicked(node),
isToggle: true,
checked: activity.canStartWorkflow,
}]
}, {
menuItems: [
{
text: 'Cut',
clickHandler: () => this.onCutActivityClicked(node)
},
{
text: 'Copy',
clickHandler: () => this.onCopyActivityClicked(node)
}]
}, {
menuItems: [{
text: 'Delete',
clickHandler: () => this.onDeleteActivityClicked(node)
}]
}];
this.activityContextMenu.menuItems = menuItems;
this.activityContextMenu.menuItemGroups = menuItemGroups;
this.activityContextMenu.style.top = `${e.y}px`;
this.activityContextMenu.style.left = `${e.x}px`;
@ -435,6 +440,42 @@ export class FlowchartComponent implements ContainerActivityComponent {
return;
this.graphUpdated.emit({exportGraph: this.exportRootInternal});
}
private onToggleCanStartWorkflowClicked = (node: ActivityNodeShape) => {
const activity = node.data as Activity;
activity.canStartWorkflow = !activity.canStartWorkflow;
node.activity = {...activity};
this.onGraphChanged().then(_ => {
});
};
private onDeleteActivityClicked = (node: ActivityNodeShape) => {
let cells = this.graph.getSelectedCells();
if (cells.length == 0)
cells = [node];
this.graph.removeCells(cells);
};
private onCopyActivityClicked = (node: ActivityNodeShape) => {
let cells = this.graph.getSelectedCells();
if (cells.length == 0)
cells = [node];
this.graph.copy(cells);
};
private onCutActivityClicked = (node: ActivityNodeShape) => {
let cells = this.graph.getSelectedCells();
if (cells.length == 0)
cells = [node];
this.graph.cut(cells);
};
}
WorkflowEditorTunnel.injectProps(FlowchartComponent, ['activityDescriptors']);
WorkflowEditorTunnel
.injectProps(FlowchartComponent, ['activityDescriptors']);

View file

@ -168,14 +168,14 @@ export function createGraph(
graph.bindKey(['meta+v', 'ctrl+v'], async () => {
if (!graph.isClipboardEmpty()) {
debugger;
disableEvents();
const cells = graph.paste({offset: 32});
debugger;
for (const cell of cells) {
cell.data.id = uuid();
}
debugger;
await enableEvents(true);
graph.cleanSelection();
graph.select(cells);

View file

@ -1,6 +1,6 @@
import {Component, h, Listen, Method, Prop} from '@stencil/core';
import {leave, toggle, enter} from 'el-transition'
import {ContextMenuAnchorPoint, MenuItem} from "./models";
import {ContextMenuAnchorPoint, MenuItem, MenuItemGroup} from "./models";
import {TickIcon} from "../../icons/tooling/tick";
@Component({
@ -9,6 +9,7 @@ import {TickIcon} from "../../icons/tooling/tick";
})
export class ContextMenu {
@Prop({mutable: true}) menuItems: Array<MenuItem> = [];
@Prop({mutable: true}) menuItemGroups: Array<MenuItemGroup> = [];
@Prop() hideButton: boolean;
@Prop() anchorPoint: ContextMenuAnchorPoint;
@ -64,20 +65,21 @@ export class ContextMenu {
private getAnchorPointClass = (): string => {
switch (this.anchorPoint) {
case ContextMenuAnchorPoint.BottomLeft:
return 'origin-bottom-left';
return 'origin-bottom-left left-0';
case ContextMenuAnchorPoint.BottomRight:
return 'origin-bottom-right';
return 'origin-bottom-right right-0';
case ContextMenuAnchorPoint.TopLeft:
return 'origin-top-left';
return 'origin-top-left left-0';
case ContextMenuAnchorPoint.TopRight:
default:
return 'origin-top-right'
return 'origin-top-right right-0'
}
};
render() {
const anchorPointClass = this.getAnchorPointClass();
const menuItems = this.menuItems;
const menuItemGroups = this.menuItemGroups;
const hasAnyIcons = menuItems.find(x => !!x.icon) != null;
return (
@ -90,30 +92,50 @@ export class ContextMenu {
data-transition-leave-start="transform opacity-100 scale-100"
data-transition-leave-end="transform opacity-0 scale-95"
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">
{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> : hasAnyIcons ? <span class="mr-7"/> : undefined}
<span class="flex-grow">{menuItem.text}</span>
{isToggle && checked ? <span class="float-right"><TickIcon/></span> : undefined}
</a>
</div>
);
})}
<div class="rounded-md bg-white shadow-xs ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="project-options-menu-0">
{this.renderMenuItems(menuItems)}
{this.renderMenuItemGroups(menuItemGroups)}
</div>
</div>
</div>
);
}
renderMenuItemGroups = (menuItemGroups: Array<MenuItemGroup>) => {
if (menuItemGroups.length == 0)
return;
return menuItemGroups.map(group => this.renderMenuItems(group.menuItems));
}
renderMenuItems = (menuItems: Array<MenuItem>) => {
if (menuItems.length == 0)
return;
const hasAnyIcons = menuItems.find(x => !!x.icon) != null;
return <div class="py-1">
{menuItems.map(menuItem => {
const anchorUrl = menuItem.anchorUrl || '#';
const isToggle = menuItem.isToggle;
const checked = menuItem.checked;
return (
<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> : hasAnyIcons ? <span class="mr-7"/> : undefined}
<span class="flex-grow">{menuItem.text}</span>
{isToggle && checked ? <span class="float-right"><TickIcon/></span> : undefined}
</a>
);
})}
</div>
};
renderButton = () => {
if (this.hideButton)
return;

View file

@ -7,6 +7,9 @@
checked?: boolean;
}
export interface MenuItemGroup {
menuItems: Array<MenuItem>
}
export enum ContextMenuAnchorPoint {
TopLeft,

View file

@ -1,7 +1,7 @@
import 'reflect-metadata';
import {h} from "@stencil/core";
import {Container, Service} from "typedi";
import {ActivityTraits} from '../../models';
import {ActivityKind} from '../../models';
import {ActivityDisplayContext, ActivityDriver, ActivityIcon, ActivityIconRegistry} from '../../services';
@Service()
@ -19,7 +19,7 @@ export class DefaultActivityDriver implements ActivityDriver {
const activity = context.activity;
const canStartWorkflow = activity?.canStartWorkflow;
const text = activityDescriptor?.displayName;
const isTrigger = (activityDescriptor?.traits & ActivityTraits.Trigger) == ActivityTraits.Trigger;
const isTrigger = activityDescriptor?.kind == ActivityKind.Trigger;
const borderColor = canStartWorkflow ? isTrigger ? 'border-green-600' : 'border-blue-600' : 'border-gray-300';
const backgroundColor = canStartWorkflow ? isTrigger ? 'bg-green-400' : 'bg-blue-400' : 'bg-white';
const textColor = canStartWorkflow ? 'text-white' : 'text-gray-700';

View file

@ -10,14 +10,14 @@ export interface ActivityDescriptor {
displayName: string;
category: string;
inputProperties: Array<InputDescriptor>
traits: ActivityTraits;
kind: ActivityKind;
inPorts: Array<Port>;
outPorts: Array<Port>;
}
export enum ActivityTraits {
Action = 1,
Trigger = 2
export enum ActivityKind {
Action = 'Action',
Trigger = 'Trigger'
}
export interface PropertyDescriptor {

View file

@ -1,7 +1,7 @@
import 'reflect-metadata';
import {h} from "@stencil/core";
import {Container, Service} from "typedi";
import {ActivityTraits} from '../../models';
import {ActivityKind} from '../../models';
import {ActivityDisplayContext, ActivityDriver, ActivityIcon, ActivityIconRegistry} from '../../services';
@Service()