Incremental work on flowchart activity editing

This commit is contained in:
Sipke Schoorstra 2022-06-27 21:12:41 +02:00
parent 247dab1013
commit 41dadaaf71
8 changed files with 120 additions and 68 deletions

View file

@ -15,11 +15,11 @@ import {NodeFactory} from "./node-factory";
import {Container} from "typedi";
import {EventBus} from "../../../services";
import {ConnectionCreatedEventArgs, FlowchartEvents} from "./events";
import {ContextMenuAnchorPoint, MenuItemGroup} from "../../shared/context-menu/models";
import descriptorsStore from "../../../data/descriptors-store";
import PositionEventArgs = NodeView.PositionEventArgs;
import FromJSONData = Model.FromJSONData;
import {ContextMenuAnchorPoint, MenuItemGroup} from "../../shared/context-menu/models";
import PointLike = Point.PointLike;
import descriptorsStore from "../../../data/descriptors-store";
@Component({
tag: 'elsa-flowchart',
@ -84,8 +84,7 @@ export class FlowchartComponent implements ContainerActivityComponent {
const graph = this.graph;
const {descriptor, x, y} = args;
let id = args.id ?? uuid();
const pageToLocal = graph.pageToLocal(x, y);
const point: PointLike = pageToLocal;
const point: PointLike = graph.pageToLocal(x, y);
const sx = point.x;
const sy = point.y;
@ -218,9 +217,6 @@ export class FlowchartComponent implements ContainerActivityComponent {
flowchart.activities = activities;
flowchart.connections = connections;
flowchart.start = startActivity?.id;
flowchart.metadata = {...flowchart.metadata};
flowchart.applicationProperties = {};
flowchart.variables = [];
return flowchart;
}
@ -406,8 +402,9 @@ export class FlowchartComponent implements ContainerActivityComponent {
}
onGraphChanged = async () => {
if (this.silent)
if (this.silent) {
return;
}
this.graphUpdated.emit({exportGraph: this.exportInternal});
}

View file

@ -123,54 +123,90 @@ export class DefaultActivityTemplate {
const textColor = canStartWorkflow ? 'text-white' : 'text-gray-700';
const borderColor = port.name == this.selectedPortName ? 'border-blue-600' : 'border-gray-300';
const portName = camelCase(port.name);
const childActivity: Activity = activity ? activity[portName] : null;
const childActivityDescriptor: ActivityDescriptor = childActivity != null ? descriptorsStore.activityDescriptors.find(x => x.activityType == childActivity.typeName) : null;
let childActivityDisplayText = childActivity?.metadata?.displayText;
const activityProperty: Activity = activity ? activity[portName] : null;
const childActivityDescriptor: ActivityDescriptor = activityProperty != null ? descriptorsStore.activityDescriptors.find(x => x.activityType == activityProperty.typeName) : null;
let childActivityDisplayText = activityProperty?.metadata?.displayText;
if (isNullOrWhitespace(childActivityDisplayText))
childActivityDisplayText = childActivityDescriptor?.displayName;
const renderActivityProperty = () => {
if (!activityProperty) {
return (
<div class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-5 text-center focus:outline-none">
<a href="#"
onClick={e => this.onEditChildActivityClick(e, activity, port)}
onMouseDown={e => e.stopPropagation()}
class="text-gray-400 hover:text-gray-600">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
</a>
</div>
);
}
const propertyIsArray = Array.isArray(activityProperty);
if (!propertyIsArray) {
return (
<div class={`relative block w-full border-2 ${borderColor} border-solid rounded-lg p-5 text-center focus:outline-none`}
onMouseDown={this.onChildActivityMouseDown}
onClick={e => this.onChildActivityClick(e, activity, activityProperty, port)}>
<div class="flex space-x-2">
<div class="flex-grow">
<span class={textColor}>{childActivityDisplayText}</span>
</div>
<div class="flex-shrink">
<a
onClick={e => this.onEditChildActivityClick(e, activity, port)}
onMouseDown={e => e.stopPropagation()}
href="#"
class="text-gray-500 hover:text-yellow-700">
<svg class="h-6 w-6" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M4 20h4l10.5 -10.5a1.5 1.5 0 0 0 -4 -4l-10.5 10.5v4"/>
<line x1="13.5" y1="6.5" x2="17.5" y2="10.5"/>
</svg>
</a>
</div>
</div>
</div>
);
}
return (
<div class={`relative block w-full border-2 ${borderColor} border-solid rounded-lg p-5 text-center focus:outline-none`}>
<div class="flex space-x-2">
<div class="flex-grow">
<span class={textColor}>{childActivityDisplayText}</span>
</div>
<div class="flex-shrink">
<a
onClick={e => this.onEditChildActivityClick(e, activity, port)}
onMouseDown={e => e.stopPropagation()}
href="#"
class="text-gray-500 hover:text-yellow-700">
<svg class="h-6 w-6" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M4 20h4l10.5 -10.5a1.5 1.5 0 0 0 -4 -4l-10.5 10.5v4"/>
<line x1="13.5" y1="6.5" x2="17.5" y2="10.5"/>
</svg>
</a>
</div>
</div>
</div>
);
}
return (
<div class="activity-port" data-port-name={port.name} ref={el => this.portElements.push(el)}>
<div>
<span class={`${textColor} text-xs`}>{port.displayName}</span>
</div>
<div>
{childActivity ? (
<div class={`relative block w-full border-2 ${borderColor} border-solid rounded-lg p-5 text-center focus:outline-none`}
onMouseDown={this.onChildActivityMouseDown}
onClick={e => this.onChildActivityClick(e, activity, childActivity, port)}
>
<div class="flex space-x-2">
<div class="flex-grow">
<span class={textColor}>{childActivityDisplayText}</span>
</div>
<div class="flex-shrink">
<a
onClick={e => this.onEditChildActivityClick(e, activity, port)}
onMouseDown={e => e.stopPropagation()}
href="#"
class="text-gray-500 hover:text-yellow-700">
<svg class="h-6 w-6" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M4 20h4l10.5 -10.5a1.5 1.5 0 0 0 -4 -4l-10.5 10.5v4"/>
<line x1="13.5" y1="6.5" x2="17.5" y2="10.5"/>
</svg>
</a>
</div>
</div>
</div>
) : (
<div class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-5 text-center focus:outline-none">
<a href="#"
onClick={e => this.onEditChildActivityClick(e, activity, port)}
onMouseDown={e => e.stopPropagation()}
class="text-gray-400 hover:text-gray-600">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
</a>
</div>)}
{renderActivityProperty()}
</div>
</div>
);

View file

@ -63,11 +63,6 @@ export class WorkflowDefinitionEditor {
await this.updateLayout();
}
@Listen('collapsed')
private async handlePanelCollapsed(e: CustomEvent) {
//this.selectedActivity = null;
}
@Listen('containerSelected')
private async handleContainerSelected(e: CustomEvent<ContainerSelectedArgs>) {
this.selectedActivity = this.getCurrentContainer();
@ -114,12 +109,17 @@ export class WorkflowDefinitionEditor {
this.currentWorkflowPath = [...this.currentWorkflowPath, item];
const portName = camelCase(e.detail.port.name);
const parentActivity = this.nodeMap[parentActivityId];
const childActivity = parentActivity[portName] as Activity;
const activityProperty = parentActivity[portName] as Activity | Array<Activity>;
const isContainer = Array.isArray(activityProperty);
if (!childActivity) {
if (!activityProperty) {
await this.canvas.reset();
} else {
await this.canvas.importGraph(childActivity);
if (isContainer) {
await this.canvas.importGraph(parentActivity);
} else {
await this.canvas.importGraph(activityProperty as Activity);
}
}
this.selectedActivity = this.getCurrentContainer();
@ -152,7 +152,7 @@ export class WorkflowDefinitionEditor {
@Method()
async updateWorkflowDefinition(workflowDefinition: WorkflowDefinition): Promise<void> {
this.workflowDefinitionState = workflowDefinition;
this.nodeMap = createActivityMap(flatten(walkActivities(workflowDefinition.root)));
this.nodeMap = this.createNodeMap(workflowDefinition);
if (this.currentWorkflowPath.length == 0) {
this.currentWorkflowPath = [{activityId: workflowDefinition.root.id, portName: null}];
@ -266,7 +266,7 @@ export class WorkflowDefinitionEditor {
private getWorkflowDefinitionInternal = async (): Promise<WorkflowDefinition> => {
const activity: Activity = await this.canvas.exportGraph();
const workflowDefinition = this.workflowDefinitionState;
const nodeMap = this.nodeMap;
const nodeMap = this.nodeMap; //this.createNodeMap(workflowDefinition);
const currentWorkflowPath = this.currentWorkflowPath;
const currentWorkflowNavigationItem = currentWorkflowPath[currentWorkflowPath.length - 1];
const currentActivityId = currentWorkflowNavigationItem.activityId;
@ -279,8 +279,15 @@ export class WorkflowDefinitionEditor {
}
if (!!currentPortName) {
const portName = camelCase(currentPortName);
currentActivity[portName] = activity;
// If we're editing a container (Flowchart) activity, the exported activity will be the current activity. No need to update anything here.
if (currentActivity.id !== activity.id) {
const portName = camelCase(currentPortName);
currentActivity[portName] = activity;
this.nodeMap = this.createNodeMap(workflowDefinition)
} else {
debugger;
nodeMap[currentActivityId] = activity;
}
} else {
workflowDefinition.root = activity;
@ -336,6 +343,8 @@ export class WorkflowDefinitionEditor {
return this.nodeMap[currentItem.activityId];
};
private createNodeMap = (workflowDefinition: WorkflowDefinition) => createActivityMap(flatten(walkActivities(workflowDefinition.root)));
private onActivityPickerPanelStateChanged = async (e: PanelStateChangedArgs) => await this.updateContainerLayout('activity-picker-closed', e.expanded)
private onWorkflowEditorPanelStateChanged = async (e: PanelStateChangedArgs) => await this.updateContainerLayout('object-editor-closed', e.expanded)
private onActivityEditorPanelStateChanged = async (e: PanelStateChangedArgs) => await this.updateContainerLayout('activity-editor-closed', e.expanded)

View file

@ -81,6 +81,7 @@ export interface ActivityDescriptor {
outputs: Array<OutputDescriptor>;
kind: ActivityKind;
ports: Array<Port>;
isContainer: boolean;
}
export interface PropertyDescriptor {

View file

@ -17,10 +17,13 @@ public class Flowchart : Container
[Node] public IActivity? Start { get; set; }
public ICollection<Connection> Connections { get; set; } = new List<Connection>();
protected override void ScheduleChildren(ActivityExecutionContext context)
protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context)
{
if (Start == null!)
{
await context.CompleteActivityAsync();
return;
}
context.ScheduleActivity(Start);
}

View file

@ -9,26 +9,26 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
{
private const string AllActivitiesKey = "AllActivities";
private const string AllConnectionsKey = "AllConnections";
public override Activities.Flowchart Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!JsonDocument.TryParseValue(ref reader, out var doc))
throw new JsonException("Failed to parse JsonDocument");
var connectionsElement = doc.RootElement.GetProperty("connections");
var connectionsElement = doc.RootElement.TryGetProperty("connections", out var connectionsEl) ? connectionsEl : default;
var metadataElement = doc.RootElement.GetProperty("metadata");
var activitiesElement = doc.RootElement.GetProperty("activities");
var activitiesElement = doc.RootElement.TryGetProperty("activities", out var activitiesEl) ? activitiesEl : default;
var id = doc.RootElement.GetProperty("id").GetString()!;
var startId = doc.RootElement.TryGetProperty("start", out var startElement) ? startElement.GetString() : default;
var activities = activitiesElement.Deserialize<ICollection<IActivity>>(options) ?? new List<IActivity>();
var activities = activitiesElement.ValueKind != JsonValueKind.Undefined ? activitiesElement.Deserialize<ICollection<IActivity>>(options) ?? new List<IActivity>() : new List<IActivity>();
var metadata = metadataElement.Deserialize<IDictionary<string, object>>(options) ?? new Dictionary<string, object>();
var start = activities.FirstOrDefault(x => x.Id == startId) ?? activities.FirstOrDefault();
var connectionSerializerOptions = new JsonSerializerOptions(options);
var activityDictionary = activities.ToDictionary(x => x.Id);
connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
var connections = connectionsElement.Deserialize<ICollection<Connection>>(connectionSerializerOptions) ?? new List<Connection>();
var connections = connectionsElement.ValueKind != JsonValueKind.Undefined ? connectionsElement.Deserialize<ICollection<Connection>>(connectionSerializerOptions) ?? new List<Connection>() : new List<Connection>();
var flowChart = new Activities.Flowchart
{
Id = id,
@ -51,7 +51,7 @@ public class FlowchartJsonConverter : JsonConverter<Activities.Flowchart>
var activities = value.Activities;
var connectionSerializerOptions = new JsonSerializerOptions(options);
var activityDictionary = activities.ToDictionary(x => x.Id);
connectionSerializerOptions.Converters.Add(new ConnectionJsonConverter(activityDictionary));
var allActivities = value.ApplicationProperties.TryGetValue(AllActivitiesKey, out var a) ? a : activities;

View file

@ -12,10 +12,14 @@ public class ActivityDescriptor
public string? Description { get; init; }
public ICollection<InputDescriptor> Inputs { get; init; } = new List<InputDescriptor>();
public ICollection<OutputDescriptor> Outputs { get; init; } = new List<OutputDescriptor>();
[JsonIgnore] public Func<ActivityConstructorContext, IActivity> Constructor { get; init; } = default!;
public ActivityKind Kind { get; set; } = ActivityKind.Action;
public ICollection<Port> Ports { get; init; } = new List<Port>();
/// <summary>
/// A value indicating whether this activity is a container of child activities.
/// </summary>
public bool IsContainer { get; set; }
}
public record ActivityConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions);

View file

@ -9,6 +9,7 @@ using Elsa.Workflows.Management.Extensions;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Services;
using Humanizer;
using IContainer = Elsa.Workflows.Core.Services.IContainer;
namespace Elsa.Workflows.Management.Implementations;
@ -64,6 +65,7 @@ public class ActivityDescriber : IActivityDescriber
Ports = outboundPorts.ToList(),
Inputs = DescribeInputProperties(inputProperties).ToList(),
Outputs = DescribeOutputProperties(outputProperties).ToList(),
IsContainer = typeof(IContainer).IsAssignableFrom(activityType),
Constructor = context =>
{
var activity = _activityFactory.Create(activityType, context);