Implement activity context menu actions
This commit is contained in:
parent
4b42098513
commit
e12604db04
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
|||
7
src/core/Elsa.Management/Models/ActivityKind.cs
Normal file
7
src/core/Elsa.Management/Models/ActivityKind.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Elsa.Management.Models;
|
||||
|
||||
public enum ActivityKind
|
||||
{
|
||||
Action,
|
||||
Trigger
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Elsa.Management.Models;
|
||||
|
||||
[Flags]
|
||||
public enum ActivityTraits
|
||||
{
|
||||
Action = 1,
|
||||
Trigger = 2
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
checked?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuItemGroup {
|
||||
menuItems: Array<MenuItem>
|
||||
}
|
||||
|
||||
export enum ContextMenuAnchorPoint {
|
||||
TopLeft,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue