diff --git a/src/core/Elsa.Management/Implementations/ActivityDescriber.cs b/src/core/Elsa.Management/Implementations/ActivityDescriber.cs index 65120558b..16632f59c 100644 --- a/src/core/Elsa.Management/Implementations/ActivityDescriber.cs +++ b/src/core/Elsa.Management/Implementations/ActivityDescriber.cs @@ -52,7 +52,7 @@ public class ActivityDescriber : IActivityDescriber var properties = activityType.GetProperties(); var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute() != 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(), diff --git a/src/core/Elsa.Management/Models/ActivityDescriptor.cs b/src/core/Elsa.Management/Models/ActivityDescriptor.cs index a04d9cec5..552e0be4d 100644 --- a/src/core/Elsa.Management/Models/ActivityDescriptor.cs +++ b/src/core/Elsa.Management/Models/ActivityDescriptor.cs @@ -14,7 +14,7 @@ public class ActivityDescriptor public ICollection OutputProperties { get; init; } = new List(); [JsonIgnore] public Func Constructor { get; init; } = default!; - public ActivityTraits Traits { get; set; } = ActivityTraits.Action; + public ActivityKind Kind { get; set; } = ActivityKind.Action; public ICollection InPorts { get; init; } = new List(); public ICollection OutPorts { get; init; } = new List(); } diff --git a/src/core/Elsa.Management/Models/ActivityKind.cs b/src/core/Elsa.Management/Models/ActivityKind.cs new file mode 100644 index 000000000..43c3e0ca2 --- /dev/null +++ b/src/core/Elsa.Management/Models/ActivityKind.cs @@ -0,0 +1,7 @@ +namespace Elsa.Management.Models; + +public enum ActivityKind +{ + Action, + Trigger +} \ No newline at end of file diff --git a/src/core/Elsa.Management/Models/ActivityTraits.cs b/src/core/Elsa.Management/Models/ActivityTraits.cs deleted file mode 100644 index a4111a00f..000000000 --- a/src/core/Elsa.Management/Models/ActivityTraits.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Elsa.Management.Models; - -[Flags] -public enum ActivityTraits -{ - Action = 1, - Trigger = 2 -} \ No newline at end of file diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index c4bbc197d..df227ecaa 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 { 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; "hideButton": boolean; + "menuItemGroups": Array; "menuItems": Array; "open": () => Promise; } @@ -435,6 +436,7 @@ declare namespace LocalJSX { interface ElsaContextMenu { "anchorPoint"?: ContextMenuAnchorPoint; "hideButton"?: boolean; + "menuItemGroups"?: Array; "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 4664a8799..c46dc1855 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 @@ -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 => { @@ -358,26 +356,33 @@ export class FlowchartComponent implements ContainerActivityComponent { private onNodeContextMenu = async (e: PositionEventArgs) => { const node = e.node as ActivityNodeShape; const activity = e.node.data as Activity; - const canStartWorkflow = activity.canStartWorkflow; - const menuItems: Array = [{ - 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 = [ + { + 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']); diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/graph-factory.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/graph-factory.ts index 40728f896..0388e7f84 100644 --- a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/graph-factory.ts +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/graph-factory.ts @@ -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); 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 7b4351fe5..3f7228ef3 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,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 = []; + @Prop({mutable: true}) menuItemGroups: Array = []; @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`}> - ); } + renderMenuItemGroups = (menuItemGroups: Array) => { + if (menuItemGroups.length == 0) + return; + + return menuItemGroups.map(group => this.renderMenuItems(group.menuItems)); + } + + renderMenuItems = (menuItems: Array) => { + + if (menuItems.length == 0) + return; + + const hasAnyIcons = menuItems.find(x => !!x.icon) != null; + + return + }; + renderButton = () => { if (this.hideButton) 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 0fb8b254e..6c97f1062 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 @@ -7,6 +7,9 @@ checked?: boolean; } +export interface MenuItemGroup { + menuItems: Array +} export enum ContextMenuAnchorPoint { TopLeft, diff --git a/src/designer/elsa-workflows-designer/src/drivers/activity/default-activity-driver.tsx b/src/designer/elsa-workflows-designer/src/drivers/activity/default-activity-driver.tsx index eff909b8b..613f8e113 100644 --- a/src/designer/elsa-workflows-designer/src/drivers/activity/default-activity-driver.tsx +++ b/src/designer/elsa-workflows-designer/src/drivers/activity/default-activity-driver.tsx @@ -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'; diff --git a/src/designer/elsa-workflows-designer/src/models/api.ts b/src/designer/elsa-workflows-designer/src/models/api.ts index f81c5d3e2..9e6179cf2 100644 --- a/src/designer/elsa-workflows-designer/src/models/api.ts +++ b/src/designer/elsa-workflows-designer/src/models/api.ts @@ -10,14 +10,14 @@ export interface ActivityDescriptor { displayName: string; category: string; inputProperties: Array - traits: ActivityTraits; + kind: ActivityKind; inPorts: Array; outPorts: Array; } -export enum ActivityTraits { - Action = 1, - Trigger = 2 +export enum ActivityKind { + Action = 'Action', + Trigger = 'Trigger' } export interface PropertyDescriptor { diff --git a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-activity-driver.tsx b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-activity-driver.tsx index 27b6931db..5bb3c87c9 100644 --- a/src/designer/elsa-workflows-designer/src/plugins/switch/switch-activity-driver.tsx +++ b/src/designer/elsa-workflows-designer/src/plugins/switch/switch-activity-driver.tsx @@ -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()