Incremental work on flowchart activity editing
This commit is contained in:
parent
247dab1013
commit
41dadaaf71
|
|
@ -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});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export interface ActivityDescriptor {
|
|||
outputs: Array<OutputDescriptor>;
|
||||
kind: ActivityKind;
|
||||
ports: Array<Port>;
|
||||
isContainer: boolean;
|
||||
}
|
||||
|
||||
export interface PropertyDescriptor {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue