From 0f439c1f0d7dc7883ad797c63f54e950442f9890 Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Thu, 24 Apr 2025 11:11:15 -0400 Subject: [PATCH 1/6] Enhance flowchart handling and improve performance Updated `GetFlowchart` method in `ActivityExtensions.cs` to handle various activity types, including a new fallback for synthetic flowcharts. Modified `VisitAndMapAsync` in `ActivityVisitorExtensions.cs` to prevent node duplication. Improved `LoadFlowchartAsync` in `FlowchartDesignerWrapper.razor.cs` to unwrap flowcharts and optimize loading by checking for previously loaded charts. Updated using directives to include necessary namespaces. --- .../Extensions/ActivityExtensions.cs | 44 ++++++++++++++++--- .../Extensions/ActivityVisitorExtensions.cs | 2 +- .../FlowchartDesignerWrapper.razor.cs | 24 +++++++--- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs b/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs index f31fa4868..7e014797f 100644 --- a/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs +++ b/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs @@ -1,5 +1,6 @@ using System.Text.Json.Nodes; using Elsa.Api.Client.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Elsa.Studio.Workflows.Extensions; @@ -16,14 +17,47 @@ public static class ActivityExtensions /// public static JsonObject GetFlowchart(this JsonObject activity) { - var activityTypeName = activity.GetTypeName(); + var typeName = activity.GetTypeName(); - if (activityTypeName == "Elsa.Flowchart") + // 1) If it already *is* a flowchart, return it + if (typeName == "Elsa.Flowchart") return activity; - if (activityTypeName == "Elsa.Workflow") + // 2) If it’s the root workflow, unwrap its root flowchart + if (typeName == "Elsa.Workflow") return activity.GetRoot()!; - throw new NotSupportedException(); + // 3) Look for a child property that *is* a Flowchart + foreach (var kvp in activity) + { + if (kvp.Value is JsonObject child && child.GetTypeName() == "Elsa.Flowchart") + return child; + } + + // 4) **NEW** fallback: if there’s an "activities" array, wrap it as a synthetic flowchart + if ( + activity.TryGetPropertyValue("activities", out var arr) + && arr is JsonArray innerActivities + ) + { + var synth = new JsonObject + { + // carry over the container’s id & nodeId so selection still works + ["id"] = activity["id"]!, + ["nodeId"] = activity["nodeId"]!, + ["type"] = "Elsa.Flowchart", + ["version"] = activity["version"]!, + ["customProperties"] = new JsonObject(), + ["metadata"] = new JsonObject(), + ["activities"] = new JsonArray(innerActivities.ToArray()), + }; + + return synth; + } + + // 5) nothing matched — truly unsupported + throw new NotSupportedException( + $"Activity '{typeName}' does not contain an inner flowchart." + ); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityVisitorExtensions.cs b/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityVisitorExtensions.cs index b4d5442e6..a62ead520 100644 --- a/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityVisitorExtensions.cs +++ b/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityVisitorExtensions.cs @@ -30,7 +30,7 @@ public static async Task> VisitAndMapAsync(thi { var graph = await visitor.VisitAsync(activity); var nodes = graph.Flatten(); - return nodes.ToDictionary(x => x.NodeId); + return nodes.GroupBy(n => n.NodeId).Select(g => g.First()).ToDictionary(x => x.NodeId); } /// Creates an activity graph based on the provided workflow definition. diff --git a/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDesignerWrapper.razor.cs b/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDesignerWrapper.razor.cs index 5fb54443e..1b49cc116 100644 --- a/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDesignerWrapper.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDesignerWrapper.razor.cs @@ -6,6 +6,7 @@ using Elsa.Api.Client.Shared.Models; using Elsa.Studio.Workflows.Designer.Components; using Elsa.Studio.Workflows.Domain.Contracts; +using Elsa.Studio.Workflows.Extensions; using Elsa.Studio.Workflows.Models; using Elsa.Studio.Workflows.UI.Args; using Elsa.Studio.Workflows.UI.Models; @@ -60,19 +61,32 @@ public partial class FlowchartDesignerWrapper [Inject] private IActivityNameGenerator ActivityNameGenerator { get; set; } = default!; private FlowchartDesigner Designer { get; set; } = default!; + private string? _lastFlowchartId; + /// /// Loads the specified flowchart activity into the designer. /// /// The flowchart activity to load. /// A map of activity stats. - public async Task LoadFlowchartAsync(JsonObject activity, IDictionary? activityStats = default) + public async Task LoadFlowchartAsync( + JsonObject activity, + IDictionary? activityStats = default + ) { - if (activity.GetTypeName() != "Elsa.Flowchart") - throw new ArgumentException("Activity must be an Elsa.Flowchart", nameof(activity)); + // 1) Unwrap any container to the real Elsa.Flowchart + var flowchart = activity.GetFlowchart(); + + // 2) Bail out if it's the exact same chart we already have + var id = flowchart.GetId(); + if (id == _lastFlowchartId) + return; - Flowchart = activity; + // 3) Otherwise record and hand off + _lastFlowchartId = id; + Flowchart = flowchart; ActivityStats = activityStats; - await Designer.LoadFlowchartAsync(activity, activityStats); + + await Designer.LoadFlowchartAsync(flowchart, activityStats); } /// From f26072cf6d85a4febdb7a12e0a44db6969d92cb2 Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Thu, 24 Apr 2025 16:56:55 -0400 Subject: [PATCH 2/6] Enhance flowchart handling and activity selection logic - Updated `GetFlowchart` to return a nullable `JsonObject?` and added `FindActivitiesContainer` for better activity container retrieval. - Refactored `LoadFlowchartAsync` in `FlowchartDesigner.razor.cs` to utilize the new method, improving flowchart loading flexibility. - Added `CreateSyntheticContainer` helper to wrap single activities in an activities array. - Enhanced `AutoLayoutAsync` to check for null flowcharts before processing. - Modified flowchart unwrapping logic in `FlowchartDesignerWrapper.razor.cs` to support both flowcharts and activities containers. - Improved robustness in `InvokeDesignerActionAsync` with a null check for the action parameter. - Refactored `SelectActivityAsync` in `DiagramDesignerWrapper.razor.cs` to use an activity node lookup dictionary for more efficient activity selection. --- .../Extensions/ActivityExtensions.cs | 68 ++++++++++--------- .../Components/FlowchartDesigner.razor.cs | 39 +++++++++-- .../FlowchartDesignerWrapper.razor.cs | 4 +- .../Flowcharts/FlowchartDiagramDesigner.cs | 3 +- .../DiagramDesignerWrapper.razor.cs | 28 +++++--- 5 files changed, 95 insertions(+), 47 deletions(-) diff --git a/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs b/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs index 7e014797f..ed23e0f4a 100644 --- a/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs +++ b/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs @@ -15,49 +15,53 @@ public static class ActivityExtensions /// /// /// - public static JsonObject GetFlowchart(this JsonObject activity) + public static JsonObject? GetFlowchart(this JsonObject activity) { - var typeName = activity.GetTypeName(); + var activityTypeName = activity.GetTypeName(); - // 1) If it already *is* a flowchart, return it - if (typeName == "Elsa.Flowchart") + if (activityTypeName == "Elsa.Flowchart") return activity; - // 2) If it’s the root workflow, unwrap its root flowchart - if (typeName == "Elsa.Workflow") + if (activityTypeName == "Elsa.Workflow") return activity.GetRoot()!; - // 3) Look for a child property that *is* a Flowchart + return null; + } + + /// + /// Recursively looks for the first JsonObject that has an "activities" array. + /// If none is found, returns null. + /// + public static JsonObject? FindActivitiesContainer(this JsonObject activity) + { + // 1) If *this* object has an "activities" array, it is the container. + if (activity.TryGetPropertyValue("activities", out var maybeArr) && maybeArr is JsonArray) + return activity; + + // 2) Otherwise, scan every child property... foreach (var kvp in activity) { - if (kvp.Value is JsonObject child && child.GetTypeName() == "Elsa.Flowchart") - return child; - } + // …if it’s a JsonObject, recurse into it. + if (kvp.Value is JsonObject childObj) + { + var found = childObj.FindActivitiesContainer(); + if (found != null) + return found; + } - // 4) **NEW** fallback: if there’s an "activities" array, wrap it as a synthetic flowchart - if ( - activity.TryGetPropertyValue("activities", out var arr) - && arr is JsonArray innerActivities - ) - { - var synth = new JsonObject + // …if it’s a JsonArray, recurse into each item. + if (kvp.Value is JsonArray childArr) { - // carry over the container’s id & nodeId so selection still works - ["id"] = activity["id"]!, - ["nodeId"] = activity["nodeId"]!, - ["type"] = "Elsa.Flowchart", - ["version"] = activity["version"]!, - ["customProperties"] = new JsonObject(), - ["metadata"] = new JsonObject(), - ["activities"] = new JsonArray(innerActivities.ToArray()), - }; - - return synth; + foreach (var node in childArr.OfType()) + { + var found = node.FindActivitiesContainer(); + if (found != null) + return found; + } + } } - // 5) nothing matched — truly unsupported - throw new NotSupportedException( - $"Activity '{typeName}' does not contain an inner flowchart." - ); + // 3) No container found. + return null; } } diff --git a/src/modules/Elsa.Studio.Workflows.Designer/Components/FlowchartDesigner.razor.cs b/src/modules/Elsa.Studio.Workflows.Designer/Components/FlowchartDesigner.razor.cs index 610ab9cbb..74b1c9b53 100644 --- a/src/modules/Elsa.Studio.Workflows.Designer/Components/FlowchartDesigner.razor.cs +++ b/src/modules/Elsa.Studio.Workflows.Designer/Components/FlowchartDesigner.razor.cs @@ -224,16 +224,42 @@ public async Task ReadFlowchartAsync() /// /// The flowchart to load. /// The activity stats to load. - public async Task LoadFlowchartAsync(JsonObject activity, IDictionary? activityStats) + public async Task LoadFlowchartAsync( + JsonObject activity, + IDictionary? activityStats + ) { Flowchart = activity; ActivityStats = activityStats; + + // 2) Get the mapper as before var flowchartMapper = await GetFlowchartMapperAsync(); - var flowchart = activity.GetFlowchart(); - var graph = flowchartMapper.Map(flowchart, activityStats); + + // 3) Instead of unwrapping only Elsa.Flowchart, find any container + // with an 'activities' array (or fall back to a synthetic one) + var container = activity.FindActivitiesContainer() ?? CreateSyntheticContainer(activity); + + // 4) Map and schedule the graph load + var graph = flowchartMapper.Map(container, activityStats); await ScheduleGraphActionAsync(() => _graphApi.LoadGraphAsync(graph)); } + // Helper: wrap a single activity in an 'activities' array + // Helper: wrap a single activity in an 'activities' array + // by cloning it so it has no existing parent. + private JsonObject CreateSyntheticContainer(JsonObject single) + { + // Make a copy so we don't reparent the original + var cloned = (JsonObject)single.DeepClone()!; + + // Put the clone into a brand-new array + var arr = new JsonArray(); + arr.Add(cloned); + + // Wrap it up + return new JsonObject { ["activities"] = arr }; + } + /// /// Adds an activity to the graph. /// @@ -263,10 +289,15 @@ public async Task SelectActivityAsync(string id) public async Task CenterContentAsync() => await ScheduleGraphActionAsync(() => _graphApi.CenterContentAsync()); /// Update the Graph Layout. - public async Task AutoLayoutAsync(JsonObject activity, IDictionary? activityStats) + public async Task AutoLayoutAsync( + JsonObject activity, + IDictionary? activityStats + ) { var flowchartMapper = await GetFlowchartMapperAsync(); var flowchart = activity.GetFlowchart(); + if (flowchart == null) + return; var graph = flowchartMapper.Map(flowchart, activityStats); await ScheduleGraphActionAsync(() => _graphApi.AutoLayoutAsync(graph)); } diff --git a/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDesignerWrapper.razor.cs b/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDesignerWrapper.razor.cs index 1b49cc116..0a26289d0 100644 --- a/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDesignerWrapper.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDesignerWrapper.razor.cs @@ -74,8 +74,10 @@ public async Task LoadFlowchartAsync( ) { // 1) Unwrap any container to the real Elsa.Flowchart - var flowchart = activity.GetFlowchart(); + var flowchart = activity.GetFlowchart() ?? activity.FindActivitiesContainer(); + if (flowchart == null) + return; // 2) Bail out if it's the exact same chart we already have var id = flowchart.GetId(); if (id == _lastFlowchartId) diff --git a/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDiagramDesigner.cs b/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDiagramDesigner.cs index 57e103c10..7d246f2ef 100644 --- a/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDiagramDesigner.cs +++ b/src/modules/Elsa.Studio.Workflows/DiagramDesigners/Flowcharts/FlowchartDiagramDesigner.cs @@ -103,7 +103,8 @@ private RenderFragment DisplayToolboxItem(string title, string icon, string desc private async Task InvokeDesignerActionAsync(Func action) { - if (_designerWrapper != null) await action(_designerWrapper); + if (_designerWrapper != null && action != null) + await action(_designerWrapper); } private Task OnZoomToFitClicked() => _designerWrapper != null ? _designerWrapper.ZoomToFitAsync() : Task.CompletedTask; diff --git a/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs b/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs index 287038248..9e5765619 100644 --- a/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs @@ -82,26 +82,36 @@ public async Task SelectActivityByActivityIdAsync(string activityId) /// The ID of the activity node to select. public async Task SelectActivityAsync(string nodeId) { - var containerActivity = GetCurrentContainerActivityOrRoot(); - var activities = containerActivity.GetActivities(); - var activityToSelect = activities.FirstOrDefault(x => x.GetNodeId() == nodeId); - - await SelectActivityAsync(activityToSelect, nodeId); + // Assuming _activityGraph.ActivityNodeLookup is your latest index + if (_activityGraph.ActivityNodeLookup.TryGetValue(nodeId, out var node)) + { + var activity = node.Activity; + await SelectActivityAsync(activity, nodeId); + } } private async Task SelectActivityAsync(JsonObject? activityToSelect, string? nodeId = null) { - if (activityToSelect != null) - { - await _diagramDesigner!.SelectActivityAsync(activityToSelect.GetId()); + var targetNodeId = activityToSelect?.GetId() ?? nodeId; + if (string.IsNullOrEmpty(targetNodeId)) return; } + // Bail out if it's the same as last time + if (targetNodeId == _lastSelectedNodeId) + return; + + // Remember for the next call + _lastSelectedNodeId = targetNodeId; + if (nodeId == null) return; // Load the selected node path from the backend. - var pathSegmentsResponse = await WorkflowDefinitionService.GetPathSegmentsAsync(WorkflowDefinitionVersionId, nodeId); + var pathSegmentsResponse = await WorkflowDefinitionService.GetPathSegmentsAsync( + WorkflowDefinitionVersionId, + nodeId + ); if (pathSegmentsResponse == null) return; From b6bebfab108b564579be908b77371bcf9674b28a Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Thu, 8 May 2025 15:31:53 -0400 Subject: [PATCH 3/6] can autolayout workflowinstance --- .../Components/WorkflowInstanceDesigner.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/WorkflowInstanceDesigner.razor b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/WorkflowInstanceDesigner.razor index ca2b84a61..09d22bbb9 100644 --- a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/WorkflowInstanceDesigner.razor +++ b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/WorkflowInstanceDesigner.razor @@ -12,7 +12,7 @@ WorkflowDefinitionVersionId="@WorkflowDefinition.Id" Activity="RootActivity" ActivitySelected="OnActivitySelected" - IsReadOnly="true" + IsReadOnly="false" WorkflowInstanceId="@WorkflowInstance.Id" PathChanged="PathChanged"> From 337d842b052ac3432bd73e0610a165520e136f25 Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Thu, 8 May 2025 16:31:16 -0400 Subject: [PATCH 4/6] Improve code readability and formatting Refactor code for better readability by adding line breaks and consistent indentation. Reformat properties marked with the `[Parameter]` attribute without changing functionality. Service injections are now listed on separate lines for clarity. Adjustments made to method logic for improved clarity in conditional checks and assignments. These changes enhance maintainability while preserving existing functionality. --- .../DiagramDesignerWrapper.razor.cs | 230 +++++++++++++----- 1 file changed, 163 insertions(+), 67 deletions(-) diff --git a/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs b/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs index 9e5765619..d14913b85 100644 --- a/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs @@ -24,48 +24,79 @@ public partial class DiagramDesignerWrapper private Stack _pathSegments = new(); private JsonObject? _currentContainerActivity; private List _breadcrumbItems = new(); - private IDictionary _activityStats = new Dictionary(); + private IDictionary _activityStats = + new Dictionary(); private ActivityGraph _activityGraph = null!; - private IDictionary _indexedActivityNodes = new Dictionary(); + private IDictionary _indexedActivityNodes = + new Dictionary(); /// The workflow definition version ID. - [Parameter] public string WorkflowDefinitionVersionId { get; set; } = null!; + [Parameter] + public string WorkflowDefinitionVersionId { get; set; } = null!; /// The root activity to display. - [Parameter] public JsonObject Activity { get; set; } = null!; + [Parameter] + public JsonObject Activity { get; set; } = null!; /// Whether the designer is read-only. - [Parameter] public bool IsReadOnly { get; set; } + [Parameter] + public bool IsReadOnly { get; set; } /// The workflow instance ID, if any. - [Parameter] public string? WorkflowInstanceId { get; set; } + [Parameter] + public string? WorkflowInstanceId { get; set; } /// A custom toolbar to display. - [Parameter] public RenderFragment? CustomToolbarItems { get; set; } + [Parameter] + public RenderFragment? CustomToolbarItems { get; set; } /// Whether the designer is progressing. - [Parameter] public bool IsProgressing { get; set; } + [Parameter] + public bool IsProgressing { get; set; } /// An event raised when an activity is selected. - [Parameter] public EventCallback ActivitySelected { get; set; } + [Parameter] + public EventCallback ActivitySelected { get; set; } /// An event raised when an embedded port is selected. - [Parameter] public EventCallback GraphUpdated { get; set; } + [Parameter] + public EventCallback GraphUpdated { get; set; } /// An event raised when the path changes. - [Parameter] public EventCallback PathChanged { get; set; } + [Parameter] + public EventCallback PathChanged { get; set; } - [Inject] private IDiagramDesignerService DiagramDesignerService { get; set; } = null!; - [Inject] private IActivityDisplaySettingsRegistry ActivityDisplaySettingsRegistry { get; set; } = null!; - [Inject] private IActivityPortService ActivityPortService { get; set; } = null!; - [Inject] private IActivityRegistry ActivityRegistry { get; set; } = null!; - [Inject] private IIdentityGenerator IdentityGenerator { get; set; } = null!; - [Inject] private IActivityExecutionService ActivityExecutionService { get; set; } = null!; - [Inject] private IActivityVisitor ActivityVisitor { get; set; } = null!; - [Inject] private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; + [Inject] + private IDiagramDesignerService DiagramDesignerService { get; set; } = null!; - private ActivityPathSegment? CurrentPathSegment => _pathSegments.TryPeek(out var segment) ? segment : null; + [Inject] + private IActivityDisplaySettingsRegistry ActivityDisplaySettingsRegistry { get; set; } = null!; + + [Inject] + private IActivityPortService ActivityPortService { get; set; } = null!; + + [Inject] + private IActivityRegistry ActivityRegistry { get; set; } = null!; + + [Inject] + private IIdentityGenerator IdentityGenerator { get; set; } = null!; + + [Inject] + private IActivityExecutionService ActivityExecutionService { get; set; } = null!; + + [Inject] + private IActivityVisitor ActivityVisitor { get; set; } = null!; + + [Inject] + private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = null!; + + [Inject] + private ISnackbar Snackbar { get; set; } = null!; + + private ActivityPathSegment? CurrentPathSegment => + _pathSegments.TryPeek(out var segment) ? segment : null; + + private string? _lastSelectedNodeId; /// Selects the activity with the specified ID. /// The ID of the activity ID select. @@ -87,7 +118,7 @@ public async Task SelectActivityAsync(string nodeId) { var activity = node.Activity; await SelectActivityAsync(activity, nodeId); - } + } } private async Task SelectActivityAsync(JsonObject? activityToSelect, string? nodeId = null) @@ -95,7 +126,6 @@ private async Task SelectActivityAsync(JsonObject? activityToSelect, string? nod var targetNodeId = activityToSelect?.GetId() ?? nodeId; if (string.IsNullOrEmpty(targetNodeId)) return; - } // Bail out if it's the same as last time if (targetNodeId == _lastSelectedNodeId) @@ -106,7 +136,7 @@ private async Task SelectActivityAsync(JsonObject? activityToSelect, string? nod if (nodeId == null) return; - + // Load the selected node path from the backend. var pathSegmentsResponse = await WorkflowDefinitionService.GetPathSegmentsAsync( WorkflowDefinitionVersionId, @@ -210,7 +240,10 @@ private async Task GetCurrentContainerActivityOrRootAsync() { var lastSegment = _pathSegments.First(); var parentNodeId = lastSegment.ActivityNodeId; - var selectedActivityGraph = await WorkflowDefinitionService.FindSubgraphAsync(WorkflowDefinitionVersionId, parentNodeId); + var selectedActivityGraph = await WorkflowDefinitionService.FindSubgraphAsync( + WorkflowDefinitionVersionId, + parentNodeId + ); var propName = lastSegment.PortName.Camelize(); var selectedPortActivity = (JsonObject?)selectedActivityGraph!.Activity[propName]; activity = selectedPortActivity; @@ -225,7 +258,8 @@ private async Task IndexActivityNodes(JsonObject activity) { var visitedNode = await ActivityVisitor.VisitAsync(activity); var nodes = visitedNode.Flatten(); - foreach (var node in nodes) _indexedActivityNodes[node.NodeId] = node; + foreach (var node in nodes) + _indexedActivityNodes[node.NodeId] = node; } private JsonObject? GetCurrentContainerActivity() @@ -242,7 +276,9 @@ private async Task IndexActivityNodes(JsonObject activity) } var nodeId = lastSegment.ActivityNodeId; - var node = _indexedActivityNodes.TryGetValue(nodeId, out var activityNode) ? activityNode : null; + var node = _indexedActivityNodes.TryGetValue(nodeId, out var activityNode) + ? activityNode + : null; if (node is null) return null; @@ -262,7 +298,12 @@ public JsonObject GetParentActivity() { var lastSegment = _pathSegments.FirstOrDefault(); var nodeId = lastSegment?.ActivityNodeId; - var node = nodeId != null ? _indexedActivityNodes.TryGetValue(nodeId, out var activityNode) ? activityNode : null : null; + var node = + nodeId != null + ? _indexedActivityNodes.TryGetValue(nodeId, out var activityNode) + ? activityNode + : null + : null; return node?.Activity ?? Activity; } @@ -284,7 +325,9 @@ private async Task UpdatePathSegmentsAsync(Action> ac { var parentActivity = GetParentActivity(); var currentContainerActivity = GetCurrentContainerActivityOrRoot(); - await PathChanged.InvokeAsync(new DesignerPathChangedArgs(parentActivity, currentContainerActivity)); + await PathChanged.InvokeAsync( + new DesignerPathChangedArgs(parentActivity, currentContainerActivity) + ); } } @@ -312,15 +355,21 @@ private async Task RefreshActivityStatsAsync() if (WorkflowInstanceId != null) { var currentContainerActivity = GetCurrentContainerActivityOrRoot(); - var report = await ActivityExecutionService.GetReportAsync(WorkflowInstanceId, currentContainerActivity); - _activityStats = report.Stats.ToDictionary(x => x.ActivityNodeId, x => new ActivityStats - { - Faulted = x.IsFaulted, - Blocked = x.IsBlocked, - Completed = x.CompletedCount, - Started = x.StartedCount, - Uncompleted = x.UncompletedCount - }); + var report = await ActivityExecutionService.GetReportAsync( + WorkflowInstanceId, + currentContainerActivity + ); + _activityStats = report.Stats.ToDictionary( + x => x.ActivityNodeId, + x => new ActivityStats + { + Faulted = x.IsFaulted, + Blocked = x.IsBlocked, + Completed = x.CompletedCount, + Started = x.StartedCount, + Uncompleted = x.UncompletedCount, + } + ); } } @@ -329,7 +378,9 @@ private async Task> GetBreadcrumbItems() var breadcrumbItems = new List(); if (_pathSegments.Any()) - breadcrumbItems.Add(new BreadcrumbItem("Root", "#_root_", false, Icons.Material.Outlined.Home)); + breadcrumbItems.Add( + new BreadcrumbItem("Root", "#_root_", false, Icons.Material.Outlined.Home) + ); var nodeLookup = _indexedActivityNodes; var firstSegment = _pathSegments.FirstOrDefault(); @@ -340,7 +391,10 @@ private async Task> GetBreadcrumbItems() if (!nodeLookup.TryGetValue(activityNodeId, out var activityNode)) { - activityNode = await WorkflowDefinitionService.FindSubgraphAsync(WorkflowDefinitionVersionId, activityNodeId); + activityNode = await WorkflowDefinitionService.FindSubgraphAsync( + WorkflowDefinitionVersionId, + activityNodeId + ); } if (activityNode == null) @@ -358,7 +412,12 @@ private async Task> GetBreadcrumbItems() var disabled = segment == firstSegment; var activityDisplayText = activity.GetName() ?? activityDescriptor.DisplayName; var breadcrumbDisplayText = $"{activityDisplayText}: {embeddedPort.DisplayName}"; - var activityBreadcrumbItem = new BreadcrumbItem(breadcrumbDisplayText, $"#{activity.GetId()}", disabled, displaySettings.Icon); + var activityBreadcrumbItem = new BreadcrumbItem( + breadcrumbDisplayText, + $"#{activity.GetId()}", + disabled, + displaySettings.Icon + ); breadcrumbItems.Add(activityBreadcrumbItem); } @@ -379,14 +438,20 @@ private async Task DisplayCurrentSegmentAsync() private RenderFragment? DisplayDesigner() { - return _diagramDesigner?.DisplayDesigner(new DisplayContext( - GetCurrentContainerActivityOrRoot(), - ActivitySelected, - EventCallback.Factory.Create(this, OnActivityEmbeddedPortSelected), - EventCallback.Factory.Create(this, OnActivityDoubleClick), - EventCallback.Factory.Create(this, OnGraphUpdated), - IsReadOnly, - _activityStats)); + return _diagramDesigner?.DisplayDesigner( + new DisplayContext( + GetCurrentContainerActivityOrRoot(), + ActivitySelected, + EventCallback.Factory.Create( + this, + OnActivityEmbeddedPortSelected + ), + EventCallback.Factory.Create(this, OnActivityDoubleClick), + EventCallback.Factory.Create(this, OnGraphUpdated), + IsReadOnly, + _activityStats + ) + ); } private async Task OnActivityDoubleClick(JsonObject activity) @@ -397,7 +462,9 @@ private async Task OnActivityDoubleClick(JsonObject activity) // If the activity is a workflow definition activity, then open the workflow definition editor. if (activity.GetWorkflowDefinitionId() != null) { - await OnActivityEmbeddedPortSelected(new ActivityEmbeddedPortSelectedArgs(activity, "Root")); + await OnActivityEmbeddedPortSelected( + new ActivityEmbeddedPortSelectedArgs(activity, "Root") + ); } } @@ -405,7 +472,9 @@ private async Task OnActivityEmbeddedPortSelected(ActivityEmbeddedPortSelectedAr { var nodes = _indexedActivityNodes; var selectedActivity = args.Activity; - var activity = nodes.TryGetValue(selectedActivity.GetNodeId(), out var selectedActivityNode) ? selectedActivityNode.Activity : null; + var activity = nodes.TryGetValue(selectedActivity.GetNodeId(), out var selectedActivityNode) + ? selectedActivityNode.Activity + : null; if (activity is null) return; @@ -424,11 +493,22 @@ private async Task OnActivityEmbeddedPortSelected(ActivityEmbeddedPortSelectedAr if (activityDescriptor.CustomProperties.ContainsKey("WorkflowDefinitionVersionId")) { var parentNodeId = activity.GetNodeId(); - var selectedActivityGraph = await WorkflowDefinitionService.FindSubgraphAsync(WorkflowDefinitionVersionId, parentNodeId) ?? throw new InvalidOperationException($"Could not find selected activity graph for {parentNodeId}"); + var selectedActivityGraph = + await WorkflowDefinitionService.FindSubgraphAsync( + WorkflowDefinitionVersionId, + parentNodeId + ) + ?? throw new InvalidOperationException( + $"Could not find selected activity graph for {parentNodeId}" + ); var propName = portName.Camelize(); var selectedPortActivity = (JsonObject)selectedActivityGraph.Activity[propName]!; embeddedActivity = selectedPortActivity; - portProvider.AssignPort(args.PortName, embeddedActivity, new PortProviderContext(activityDescriptor, activity)); + portProvider.AssignPort( + args.PortName, + embeddedActivity, + new PortProviderContext(activityDescriptor, activity) + ); await IndexActivityNodes(selectedActivityGraph.Activity); } } @@ -438,7 +518,10 @@ private async Task OnActivityEmbeddedPortSelected(ActivityEmbeddedPortSelectedAr var embeddedActivityTypeName = embeddedActivity.GetTypeName(); // If the embedded activity has no designer support, then open it in the activity properties editor by raising the ActivitySelected event. - if (embeddedActivityTypeName != "Elsa.Flowchart" && embeddedActivityTypeName != "Elsa.Workflow") + if ( + embeddedActivityTypeName != "Elsa.Flowchart" + && embeddedActivityTypeName != "Elsa.Workflow" + ) { if (ActivitySelected.HasDelegate) await ActivitySelected.InvokeAsync(embeddedActivity); @@ -453,16 +536,22 @@ private async Task OnActivityEmbeddedPortSelected(ActivityEmbeddedPortSelectedAr { var embeddedActivityId = IdentityGenerator.GenerateId(); // Create a flowchart and embed it into the activity. - embeddedActivity = new JsonObject(new Dictionary - { - ["id"] = embeddedActivityId, - ["nodeId"] = $"{activity.GetNodeId()}:{embeddedActivityId}", - ["type"] = "Elsa.Flowchart", - ["version"] = 1, - ["name"] = "Flowchart1", - }); - - portProvider.AssignPort(args.PortName, embeddedActivity, new PortProviderContext(activityDescriptor, activity)); + embeddedActivity = new JsonObject( + new Dictionary + { + ["id"] = embeddedActivityId, + ["nodeId"] = $"{activity.GetNodeId()}:{embeddedActivityId}", + ["type"] = "Elsa.Flowchart", + ["version"] = 1, + ["name"] = "Flowchart1", + } + ); + + portProvider.AssignPort( + args.PortName, + embeddedActivity, + new PortProviderContext(activityDescriptor, activity) + ); // Update the graph in the designer. await _diagramDesigner!.UpdateActivityAsync(activity.GetId(), activity); @@ -474,7 +563,12 @@ private async Task OnActivityEmbeddedPortSelected(ActivityEmbeddedPortSelectedAr } // Create a new path segment of the container activity and push it onto the stack. - var segment = new ActivityPathSegment(activity.GetNodeId(), activity.GetId(), activity.GetTypeName(), args.PortName); + var segment = new ActivityPathSegment( + activity.GetNodeId(), + activity.GetId(), + activity.GetTypeName(), + args.PortName + ); await UpdatePathSegmentsAsync(segments => segments.Push(segment)); await DisplayCurrentSegmentAsync(); @@ -492,7 +586,9 @@ private async Task OnGraphUpdated() } else { - var currentActivityNode = _activityGraph.ActivityNodeLookup[currentSegment.ActivityNodeId]; + var currentActivityNode = _activityGraph.ActivityNodeLookup[ + currentSegment.ActivityNodeId + ]; var currentActivity = currentActivityNode.Activity; var portName = currentSegment.PortName; var activityTypeName = currentActivity.GetTypeName(); @@ -533,4 +629,4 @@ await UpdatePathSegmentsAsync(segments => _currentContainerActivity = null; await DisplayCurrentSegmentAsync(); } -} \ No newline at end of file +} From 6e6a96dce7488e1680bfd21d57fb0916e6ec143f Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Tue, 13 May 2025 15:00:47 -0400 Subject: [PATCH 5/6] Fix ability to reach nested activties in deisgner with programmatic workflows --- .../Resolvers/DefaultActivityResolver.cs | 66 +++++++++++++++---- .../DiagramDesignerWrapper.razor.cs | 22 ++++--- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/modules/Elsa.Studio.Workflows.Core/Resolvers/DefaultActivityResolver.cs b/src/modules/Elsa.Studio.Workflows.Core/Resolvers/DefaultActivityResolver.cs index 2cbb070a7..6a147f663 100644 --- a/src/modules/Elsa.Studio.Workflows.Core/Resolvers/DefaultActivityResolver.cs +++ b/src/modules/Elsa.Studio.Workflows.Core/Resolvers/DefaultActivityResolver.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; using Elsa.Api.Client.Extensions; using Elsa.Studio.Workflows.Domain.Contracts; using Elsa.Studio.Workflows.Domain.Models; @@ -17,17 +17,57 @@ public class DefaultActivityResolver : IActivityResolver public bool GetSupportsActivity(JsonObject activity) => true; /// - public ValueTask> GetActivitiesAsync(JsonObject activity, CancellationToken cancellationToken = default) + public ValueTask> GetActivitiesAsync( + JsonObject activity, + CancellationToken cancellationToken = default + ) { - var containedActivities = - from prop in activity - where prop.Value is JsonObject jsonObject && jsonObject.IsActivity() || prop.Value is JsonArray - let isCollection = prop.Value is JsonArray - let containedItems = isCollection ? ((JsonArray)prop.Value).ToArray() : new[] { prop.Value.AsObject() } - from containedItem in containedItems where containedItem is JsonObject && containedItem.AsObject().IsActivity() - let containedObject = containedItem.AsObject() - select new EmbeddedActivity(containedObject, prop.Key); - - return new(containedActivities.ToList()); + var embedded = new List(); + + foreach (var (propName, node) in activity) + { + switch (node) + { + // 1) direct child activity: + case JsonObject childObj when childObj.IsActivity(): + embedded.Add(new EmbeddedActivity(childObj, propName)); + break; + + // 2) array of activities: + case JsonArray arr: + foreach (var item in arr.OfType()) + { + // e.g. expectedStatusCodes: pick up item.activity + if ( + item.TryGetPropertyValue("activity", out var actNode) + && actNode is JsonObject actObj + && actObj.IsActivity() + ) + { + embedded.Add(new EmbeddedActivity(actObj, propName)); + } + else if (item.IsActivity()) + { + embedded.Add(new EmbeddedActivity(item, propName)); + } + } + break; + } + + // 3) special‑case any nested Flowchart under "body": + if ( + node is JsonObject container + && container.TryGetPropertyValue("body", out var bodyNode) + && bodyNode is JsonObject bodyObj + && bodyObj.TryGetPropertyValue("activities", out var activitiesNode) + && activitiesNode is JsonArray childActivities + ) + { + foreach (var child in childActivities.OfType()) + embedded.Add(new EmbeddedActivity(child, propName)); + } + } + + return new ValueTask>(embedded); } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs b/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs index d14913b85..9df23156b 100644 --- a/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs @@ -456,7 +456,7 @@ private async Task DisplayCurrentSegmentAsync() private async Task OnActivityDoubleClick(JsonObject activity) { - if (!IsReadOnly) + if (IsReadOnly) return; // If the activity is a workflow definition activity, then open the workflow definition editor. @@ -515,20 +515,22 @@ await WorkflowDefinitionService.FindSubgraphAsync( if (embeddedActivity != null) { - var embeddedActivityTypeName = embeddedActivity.GetTypeName(); - - // If the embedded activity has no designer support, then open it in the activity properties editor by raising the ActivitySelected event. - if ( - embeddedActivityTypeName != "Elsa.Flowchart" - && embeddedActivityTypeName != "Elsa.Workflow" - ) + var childDescriptor = ActivityRegistry.Find( + embeddedActivity.GetTypeName(), + embeddedActivity.GetVersion() + )!; + var childPortContext = new PortProviderContext(childDescriptor, embeddedActivity); + var childPortProvider = ActivityPortService.GetProvider(childPortContext); + var childPorts = childPortProvider.GetPorts(childPortContext); + + // if it has _no_ ports, it’s just a leaf — show the property editor: + if (!childPorts.Any()) { if (ActivitySelected.HasDelegate) await ActivitySelected.InvokeAsync(embeddedActivity); + return; } - - // If the embedded activity type is a flowchart or workflow, we can display it in the designer. } else { From fa7a4021370bd85bd68195e160e9a33bc124e485 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 19 Dec 2025 20:30:30 +0100 Subject: [PATCH 6/6] Update src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs --- .../Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs b/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs index 82baf8ed7..b5d1acc17 100644 --- a/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs +++ b/src/modules/Elsa.Studio.Workflows.Core/Extensions/ActivityExtensions.cs @@ -1,6 +1,5 @@ using System.Text.Json.Nodes; using Elsa.Api.Client.Extensions; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Elsa.Studio.Workflows.Extensions;