From 41dadaaf71fb72e7865335ff54c938e5d48aff42 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 27 Jun 2022 21:12:41 +0200 Subject: [PATCH] Incremental work on flowchart activity editing --- .../activities/flowchart/flowchart.tsx | 13 +- .../default-activity-template.tsx | 112 ++++++++++++------ .../workflow-definition-editor.tsx | 33 ++++-- .../src/models/core.ts | 1 + .../Flowchart/Activities/Flowchart.cs | 5 +- .../Serialization/FlowchartJsonConverter.cs | 16 +-- .../Models/ActivityDescriptor.cs | 6 +- .../Implementations/ActivityDescriber.cs | 2 + 8 files changed, 120 insertions(+), 68 deletions(-) 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 f718fdeac..7766229c0 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 @@ -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}); } diff --git a/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx b/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx index 45843669c..ca127b093 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/activity-templates/default-activity-template.tsx @@ -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 ( +
+ this.onEditChildActivityClick(e, activity, port)} + onMouseDown={e => e.stopPropagation()} + class="text-gray-400 hover:text-gray-600"> + + + + +
+ ); + } + + const propertyIsArray = Array.isArray(activityProperty); + + if (!propertyIsArray) { + return ( +
this.onChildActivityClick(e, activity, activityProperty, port)}> + +
+ ); + } + + return ( +
+ +
+ ); + } + return (
this.portElements.push(el)}>
{port.displayName}
- {childActivity ? ( -
this.onChildActivityClick(e, activity, childActivity, port)} - > - -
- ) : ( - )} + {renderActivityProperty()}
); diff --git a/src/designer/elsa-workflows-designer/src/components/designer/workflow-definition-editor/workflow-definition-editor.tsx b/src/designer/elsa-workflows-designer/src/components/designer/workflow-definition-editor/workflow-definition-editor.tsx index 134686bd4..d013e96b1 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/workflow-definition-editor/workflow-definition-editor.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/workflow-definition-editor/workflow-definition-editor.tsx @@ -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) { 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; + 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 { 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 => { 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) diff --git a/src/designer/elsa-workflows-designer/src/models/core.ts b/src/designer/elsa-workflows-designer/src/models/core.ts index ff689088c..2ccd8ed4b 100644 --- a/src/designer/elsa-workflows-designer/src/models/core.ts +++ b/src/designer/elsa-workflows-designer/src/models/core.ts @@ -81,6 +81,7 @@ export interface ActivityDescriptor { outputs: Array; kind: ActivityKind; ports: Array; + isContainer: boolean; } export interface PropertyDescriptor { diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 6bb78a13a..2ecffe14c 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -17,10 +17,13 @@ public class Flowchart : Container [Node] public IActivity? Start { get; set; } public ICollection Connections { get; set; } = new List(); - protected override void ScheduleChildren(ActivityExecutionContext context) + protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context) { if (Start == null!) + { + await context.CompleteActivityAsync(); return; + } context.ScheduleActivity(Start); } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs index 136cb0d28..55d77c391 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Serialization/FlowchartJsonConverter.cs @@ -9,26 +9,26 @@ public class FlowchartJsonConverter : JsonConverter { 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>(options) ?? new List(); + var activities = activitiesElement.ValueKind != JsonValueKind.Undefined ? activitiesElement.Deserialize>(options) ?? new List() : new List(); var metadata = metadataElement.Deserialize>(options) ?? new Dictionary(); 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>(connectionSerializerOptions) ?? new List(); - + var connections = connectionsElement.ValueKind != JsonValueKind.Undefined ? connectionsElement.Deserialize>(connectionSerializerOptions) ?? new List() : new List(); + var flowChart = new Activities.Flowchart { Id = id, @@ -51,7 +51,7 @@ public class FlowchartJsonConverter : JsonConverter 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; diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs index 39231adb2..e5fdcc4df 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityDescriptor.cs @@ -12,10 +12,14 @@ public class ActivityDescriptor public string? Description { get; init; } public ICollection Inputs { get; init; } = new List(); public ICollection Outputs { get; init; } = new List(); - [JsonIgnore] public Func Constructor { get; init; } = default!; public ActivityKind Kind { get; set; } = ActivityKind.Action; public ICollection Ports { get; init; } = new List(); + + /// + /// A value indicating whether this activity is a container of child activities. + /// + public bool IsContainer { get; set; } } public record ActivityConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs index 0108e5dc2..02024d6e7 100644 --- a/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs @@ -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);