From 0f439c1f0d7dc7883ad797c63f54e950442f9890 Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Thu, 24 Apr 2025 11:11:15 -0400 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 be59f66b46f9f9ce5697036411a8f53c1ce6ca8a Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Thu, 15 May 2025 15:09:37 -0400 Subject: [PATCH 06/10] Enhance graph interactivity and layout handling - Updated `createGraph` to allow all nodes, edges, and vertexes to be movable regardless of read-only state. - Added automatic layout adjustment in `FlowchartDesigner` for read-only mode when all nodes are at origin. - Introduced `AllNodesAtOrigin` method to check if all activities are positioned at the origin (0,0). --- .../src/designer/api/create-graph.ts | 10 +++--- .../Components/FlowchartDesigner.razor.cs | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/modules/Elsa.Studio.Workflows.Designer/ClientLib/src/designer/api/create-graph.ts b/src/modules/Elsa.Studio.Workflows.Designer/ClientLib/src/designer/api/create-graph.ts index 1bd48f527..00f17cd70 100644 --- a/src/modules/Elsa.Studio.Workflows.Designer/ClientLib/src/designer/api/create-graph.ts +++ b/src/modules/Elsa.Studio.Workflows.Designer/ClientLib/src/designer/api/create-graph.ts @@ -37,13 +37,13 @@ export async function createGraph(containerId: string, componentRef: DotNetCompo maxScale: 3, }, interacting: { - nodeMovable: () => !readOnly, + nodeMovable: () => true, arrowheadMovable: () => !readOnly, - edgeMovable: () => !readOnly, - vertexMovable: () => !readOnly, + edgeMovable: () => true, + vertexMovable: () => true, vertexAddable: () => !readOnly, vertexDeletable: () => !readOnly, - edgeLabelMovable: () => !readOnly, + edgeLabelMovable: () => true, magnetConnectable: () => !readOnly, toolsAddable: () => !readOnly, useEdgeTools: () => !readOnly, @@ -135,7 +135,7 @@ export async function createGraph(containerId: string, componentRef: DotNetCompo rubberEdge: false, rubberNode: true, rubberband: true, - movable: !readOnly, + movable: true, showNodeSelectionBox: true }), ); 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 74b1c9b53..5f0dde9fa 100644 --- a/src/modules/Elsa.Studio.Workflows.Designer/Components/FlowchartDesigner.razor.cs +++ b/src/modules/Elsa.Studio.Workflows.Designer/Components/FlowchartDesigner.razor.cs @@ -331,6 +331,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) _componentRef = DotNetObjectReference.Create(this); _graphApi = await DesignerJsInterop.CreateGraphAsync(_containerId, _componentRef, IsReadOnly); await _pendingGraphActions.ProcessAsync(); + if (IsReadOnly && AllNodesAtOrigin(Flowchart)) + { + await AutoLayoutAsync(Flowchart, ActivityStats); + } } } @@ -433,4 +437,34 @@ void IDisposable.Dispose() ThemeService.IsDarkModeChanged -= OnDarkModeChanged; _componentRef?.Dispose(); } + + private bool AllNodesAtOrigin(JsonObject flowchart) + { + var activities = flowchart.GetActivities(); // extension that pulls the “activities” array + foreach (var activity in activities) + { + // drill into metadata.designer.position.{x,y} + if ( + activity.TryGetPropertyValue("metadata", out var metaNode) + && metaNode is JsonObject meta + && meta.TryGetPropertyValue("designer", out var designerNode) + && designerNode is JsonObject designer + && designer.TryGetPropertyValue("position", out var posNode) + && posNode is JsonObject pos + ) + { + var x = pos["x"]?.GetValue() ?? 0; + var y = pos["y"]?.GetValue() ?? 0; + if (x != 0 || y != 0) + return false; + } + else + { + // no metadata or no position? treat that as “still at origin” + continue; + } + } + + return true; + } } \ No newline at end of file From 102a5f156b50f3c1dfd281fa8235d7790b5d7a4c Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Mon, 2 Jun 2025 10:34:57 -0400 Subject: [PATCH 07/10] update latest --- Directory.Build.targets | 8 + Directory.Packages.props | 2 +- .../Elsa.Studio.Core/Elsa.Studio.Core.csproj | 10 +- .../WorkflowInstanceList.razor.cs | 253 +++++++++++++----- 4 files changed, 192 insertions(+), 81 deletions(-) create mode 100644 Directory.Build.targets diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..941a125ca --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 41509db66..c7ea34fcb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ - + diff --git a/src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj b/src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj index b523d3ffc..e64f2d306 100644 --- a/src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj +++ b/src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj @@ -25,12 +25,8 @@ - - - - - - - + + + diff --git a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs index 676536629..9790d50e8 100644 --- a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs @@ -2,21 +2,21 @@ using Elsa.Api.Client.Resources.Alterations.Models; using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; using Elsa.Api.Client.Resources.WorkflowInstances.Enums; +using Elsa.Api.Client.Resources.WorkflowInstances.Models; using Elsa.Api.Client.Resources.WorkflowInstances.Requests; using Elsa.Api.Client.Shared.Models; using Elsa.Studio.Contracts; using Elsa.Studio.DomInterop.Contracts; +using Elsa.Studio.Localization; using Elsa.Studio.Workflows.Components.WorkflowInstanceList.Components; using Elsa.Studio.Workflows.Components.WorkflowInstanceList.Models; using Elsa.Studio.Workflows.Domain.Contracts; using Humanizer; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; +using Microsoft.Extensions.Logging; using MudBlazor; using Refit; -using Elsa.Api.Client.Resources.WorkflowInstances.Models; -using Microsoft.Extensions.Logging; -using Elsa.Studio.Localization; namespace Elsa.Studio.Workflows.Components.WorkflowInstanceList; @@ -31,19 +31,37 @@ public partial class WorkflowInstanceList : IAsyncDisposable /// /// An event that is invoked when a workflow definition is edited. /// - [Parameter] public EventCallback ViewWorkflowInstance { get; set; } + [Parameter] + public EventCallback ViewWorkflowInstance { get; set; } + + [Inject] + private IDialogService DialogService { get; set; } = default!; + + [Inject] + private ISnackbar Snackbar { get; set; } = default!; - [Inject] private IDialogService DialogService { get; set; } = default!; - [Inject] private ISnackbar Snackbar { get; set; } = default!; - [Inject] private IWorkflowInstanceService WorkflowInstanceService { get; set; } = default!; - [Inject] private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = default!; - [Inject] private IBackendApiClientProvider BackendApiClientProvider { get; set; } = default!; - [Inject] private IFiles Files { get; set; } = default!; - [Inject] private IDomAccessor DomAccessor { get; set; } = default!; - [Inject] private ILogger Logger { get; set; } = default!; + [Inject] + private IWorkflowInstanceService WorkflowInstanceService { get; set; } = default!; - private ICollection WorkflowDefinitions { get; set; } = new List(); - private ICollection SelectedWorkflowDefinitions { get; set; } = new List(); + [Inject] + private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = default!; + + [Inject] + private IBackendApiClientProvider BackendApiClientProvider { get; set; } = default!; + + [Inject] + private IFiles Files { get; set; } = default!; + + [Inject] + private IDomAccessor DomAccessor { get; set; } = default!; + + [Inject] + private ILogger Logger { get; set; } = default!; + + private ICollection WorkflowDefinitions { get; set; } = + new List(); + private ICollection SelectedWorkflowDefinitions { get; set; } = + new List(); private string SearchTerm { get; set; } = string.Empty; private bool? HasIncidents { get; set; } @@ -53,24 +71,28 @@ public partial class WorkflowInstanceList : IAsyncDisposable private ICollection SelectedStatuses { get; set; } = new List(); /// The selected sub-statuses to filter by. - private ICollection SelectedSubStatuses { get; set; } = new List(); + private ICollection SelectedSubStatuses { get; set; } = + new List(); // The selected timestamp filters to filter by. - private ICollection TimestampFilters { get; set; } = new List(); - + private ICollection TimestampFilters { get; set; } = + new List(); /// protected override async Task OnInitializedAsync() { await LoadWorkflowDefinitionsAsync(); - // Disable auto refresh until we implement a way to maintain the selected state, pagination etc. + // Disable auto refresh until we implement a way to maintain the selected state, pagination etc. //StartElapsedTimer(); } private async Task LoadWorkflowDefinitionsAsync() { - var workflowDefinitionsResponse = await WorkflowDefinitionService.ListAsync(new(), VersionOptions.LatestOrPublished); + var workflowDefinitionsResponse = await WorkflowDefinitionService.ListAsync( + new(), + VersionOptions.LatestOrPublished + ); var workflowDefinitions = workflowDefinitionsResponse.Items; // Filter the definitions to ensure only the one with the highest version for each DefinitionId remains @@ -82,7 +104,10 @@ private async Task LoadWorkflowDefinitionsAsync() WorkflowDefinitions = filteredWorkflowDefinitions; } - private async Task> LoadData(TableState state, CancellationToken cancellationToken) + private async Task> LoadData( + TableState state, + CancellationToken cancellationToken + ) { var request = new ListWorkflowInstancesRequest { @@ -95,26 +120,45 @@ private async Task> LoadData(TableState state, Ca HasIncidents = HasIncidents, IsSystem = false, OrderBy = GetOrderBy(state.SortLabel), - OrderDirection = state.SortDirection == SortDirection.Descending ? OrderDirection.Descending : OrderDirection.Ascending, - TimestampFilters = TimestampFilters.Select(Map).Where(x => x.Timestamp.Date > DateTime.MinValue && !string.IsNullOrWhiteSpace(x.Column)).ToList() + OrderDirection = + state.SortDirection == SortDirection.Descending + ? OrderDirection.Descending + : OrderDirection.Ascending, + TimestampFilters = TimestampFilters + .Select(Map) + .Where(x => + x.Timestamp.Date > DateTime.MinValue && !string.IsNullOrWhiteSpace(x.Column) + ) + .ToList(), }; try { - var workflowInstancesResponse = await WorkflowInstanceService.ListAsync(request, cancellationToken); - var definitionVersionIds = workflowInstancesResponse.Items.Select(x => x.DefinitionVersionId).Distinct().ToList(); - - var workflowDefinitionVersionsResponse = await WorkflowDefinitionService.ListAsync(new() - { - Ids = definitionVersionIds, - }, cancellationToken: cancellationToken); - - var workflowDefinitionVersionsLookup = workflowDefinitionVersionsResponse.Items.ToDictionary(x => x.Id); + var workflowInstancesResponse = await WorkflowInstanceService.ListAsync( + request, + cancellationToken + ); + var definitionVersionIds = workflowInstancesResponse + .Items.Select(x => x.DefinitionVersionId) + .Distinct() + .ToList(); + + var workflowDefinitionVersionsResponse = await WorkflowDefinitionService.ListAsync( + new() { Ids = definitionVersionIds }, + cancellationToken: cancellationToken + ); + + var workflowDefinitionVersionsLookup = + workflowDefinitionVersionsResponse.Items.ToDictionary(x => x.Id); // Select any workflow instances for which no corresponding workflow definition version was found. // This can happen when a workflow definition is deleted. - var missingWorkflowDefinitionVersionIds = definitionVersionIds.Except(workflowDefinitionVersionsLookup.Keys).ToList(); - var filteredWorkflowInstances = workflowInstancesResponse.Items.Where(x => !missingWorkflowDefinitionVersionIds.Contains(x.DefinitionVersionId)); + var missingWorkflowDefinitionVersionIds = definitionVersionIds + .Except(workflowDefinitionVersionsLookup.Keys) + .ToList(); + var filteredWorkflowInstances = workflowInstancesResponse.Items.Where(x => + !missingWorkflowDefinitionVersionIds.Contains(x.DefinitionVersionId) + ); var rows = filteredWorkflowInstances.Select(x => new WorkflowInstanceRow( x.Id, @@ -127,7 +171,8 @@ private async Task> LoadData(TableState state, Ca x.IncidentCount, x.CreatedAt, x.UpdatedAt, - x.FinishedAt)); + x.FinishedAt + )); _totalCount = (int)workflowInstancesResponse.TotalCount; return new() { TotalItems = _totalCount, Items = rows }; @@ -144,10 +189,12 @@ private async Task> LoadData(TableState state, Ca _totalCount = 0; return new() { TotalItems = 0, Items = Array.Empty() }; } - } - private async Task> TryListWorkflowDefinitionsAsync(ListWorkflowInstancesRequest request, CancellationToken cancellationToken) + private async Task> TryListWorkflowDefinitionsAsync( + ListWorkflowInstancesRequest request, + CancellationToken cancellationToken + ) { try { @@ -156,17 +203,18 @@ private async Task> TryListWorkflowDe catch (ApiException ex) when (ex.InnerException is TaskCanceledException) { Logger.LogWarning("Failed to list workflow instances due to a timeout."); - return new() - { - Items = [] - }; + return new() { Items = [] }; } } private TimestampFilter Map(TimestampFilterModel source) { - var date = !string.IsNullOrWhiteSpace(source.Date) ? DateTime.Parse(source.Date) : DateTime.MinValue; - var time = !string.IsNullOrWhiteSpace(source.Time) ? TimeSpan.Parse(source.Time) : TimeSpan.Zero; + var date = !string.IsNullOrWhiteSpace(source.Date) + ? DateTime.Parse(source.Date) + : DateTime.MinValue; + var time = !string.IsNullOrWhiteSpace(source.Time) + ? TimeSpan.Parse(source.Time) + : TimeSpan.Zero; var dateTime = date.Add(time); var timestamp = dateTime == DateTime.MinValue ? DateTimeOffset.MinValue : new(dateTime); @@ -174,7 +222,7 @@ private TimestampFilter Map(TimestampFilterModel source) { Column = source.Column, Operator = source.Operator, - Timestamp = timestamp + Timestamp = timestamp, }; } @@ -186,7 +234,7 @@ private TimestampFilter Map(TimestampFilterModel source) "Finished" => OrderByWorkflowInstance.Finished, "Created" => OrderByWorkflowInstance.Created, "LastExecuted" => OrderByWorkflowInstance.LastExecuted, - _ => null + _ => null, }; } @@ -197,19 +245,21 @@ private async Task ViewAsync(string instanceId) private void Reload() => _table.ReloadServerData(); - private bool FilterWorkflowDefinitions(WorkflowDefinitionSummary workflowDefinition, string term) + private bool FilterWorkflowDefinitions( + WorkflowDefinitionSummary workflowDefinition, + string term + ) { var trimmedTerm = term.Trim(); if (string.IsNullOrEmpty(term)) return true; - var sources = new[] - { - (string?)workflowDefinition.Name - }; + var sources = new[] { (string?)workflowDefinition.Name }; - return sources.Any(x => x?.Contains(trimmedTerm, StringComparison.OrdinalIgnoreCase) == true); + return sources.Any(x => + x?.Contains(trimmedTerm, StringComparison.OrdinalIgnoreCase) == true + ); } private Color GetSubStatusColor(WorkflowSubStatus subStatus) @@ -237,11 +287,18 @@ private void ToggleDateRangePopover() } private void OnViewClicked(string instanceId) => _ = ViewAsync(instanceId); - private void OnRowClick(TableRowClickEventArgs e) => _ = ViewAsync(e.Item!.WorkflowInstanceId); + + private void OnRowClick(TableRowClickEventArgs e) => + _ = ViewAsync(e.Item!.WorkflowInstanceId); private async Task OnDeleteClicked(WorkflowInstanceRow row) { - var result = await DialogService.ShowMessageBox(Localizer["Delete workflow instance?"], Localizer["Are you sure you want to delete this workflow instance?"], yesText: Localizer["Delete"], cancelText: Localizer["Cancel"]); + var result = await DialogService.ShowMessageBox( + Localizer["Delete workflow instance?"], + Localizer["Are you sure you want to delete this workflow instance?"], + yesText: Localizer["Delete"], + cancelText: Localizer["Cancel"] + ); if (result != true) return; @@ -253,7 +310,12 @@ private async Task OnDeleteClicked(WorkflowInstanceRow row) private async Task OnCancelClicked(WorkflowInstanceRow row) { - var result = await DialogService.ShowMessageBox(Localizer["Cancel workflow instance?"], Localizer["Are you sure you want to cancel this workflow instance?"], yesText: Localizer["Yes"], cancelText: Localizer["No"]); + var result = await DialogService.ShowMessageBox( + Localizer["Cancel workflow instance?"], + Localizer["Are you sure you want to cancel this workflow instance?"], + yesText: Localizer["Yes"], + cancelText: Localizer["No"] + ); if (result != true) return; @@ -265,14 +327,22 @@ private async Task OnCancelClicked(WorkflowInstanceRow row) private async Task OnDownloadClicked(WorkflowInstanceRow workflowInstanceRow) { - var download = await WorkflowInstanceService.ExportAsync(workflowInstanceRow.WorkflowInstanceId); - var fileName = $"{workflowInstanceRow.Name?.Kebaberize() ?? workflowInstanceRow.WorkflowInstanceId}.json"; + var download = await WorkflowInstanceService.ExportAsync( + workflowInstanceRow.WorkflowInstanceId + ); + var fileName = + $"{workflowInstanceRow.Name?.Kebaberize() ?? workflowInstanceRow.WorkflowInstanceId}.json"; await Files.DownloadFileFromStreamAsync(fileName, download.Content); } private async Task OnBulkDeleteClicked() { - var result = await DialogService.ShowMessageBox(Localizer["Delete selected workflow instances?"], Localizer["Are you sure you want to delete the selected workflow instances?"], yesText: Localizer["Delete"], cancelText: Localizer["Cancel"]); + var result = await DialogService.ShowMessageBox( + Localizer["Delete selected workflow instances?"], + Localizer["Are you sure you want to delete the selected workflow instances?"], + yesText: Localizer["Delete"], + cancelText: Localizer["Cancel"] + ); if (result != true) return; @@ -285,9 +355,11 @@ private async Task OnBulkDeleteClicked() private async Task OnBulkCancelClicked() { - var reference = await DialogService.ShowAsync(Localizer["Cancel selected workflow instances?"]); + var reference = await DialogService.ShowAsync( + Localizer["Cancel selected workflow instances?"] + ); var dialogResult = await reference.Result; - + if (dialogResult == null || dialogResult.Canceled) return; @@ -297,7 +369,7 @@ private async Task OnBulkCancelClicked() { var plan = new AlterationPlanParams { - Alterations = [new Cancel()], + Alterations = [], Filter = new() { EmptyFilterSelectsAll = true, @@ -306,22 +378,36 @@ private async Task OnBulkCancelClicked() SearchTerm = SearchTerm, Statuses = SelectedStatuses.Any() ? SelectedStatuses : null, SubStatuses = SelectedSubStatuses.Any() ? SelectedSubStatuses : null, - TimestampFilters = TimestampFilters.Any() ? TimestampFilters.Select(Map).Where(x => x.Timestamp.Date > DateTime.MinValue && !string.IsNullOrWhiteSpace(x.Column)).ToList() : null, - DefinitionIds = SelectedWorkflowDefinitions.Any() ? SelectedWorkflowDefinitions.Select(x => x.DefinitionId).ToList() : null - } + TimestampFilters = TimestampFilters.Any() + ? TimestampFilters + .Select(Map) + .Where(x => + x.Timestamp.Date > DateTime.MinValue + && !string.IsNullOrWhiteSpace(x.Column) + ) + .ToList() + : null, + DefinitionIds = SelectedWorkflowDefinitions.Any() + ? SelectedWorkflowDefinitions.Select(x => x.DefinitionId).ToList() + : null, + }, }; var alterationsApi = await BackendApiClientProvider.GetApiAsync(); await alterationsApi.Submit(plan); - Snackbar.Add("Workflow instances are being cancelled.", Severity.Info, options => { options.SnackbarVariant = Variant.Filled; }); + Snackbar.Add( + "Workflow instances are being cancelled.", + Severity.Info, + options => + { + options.SnackbarVariant = Variant.Filled; + } + ); } else { var workflowInstanceIds = _selectedRows.Select(x => x.WorkflowInstanceId).ToList(); - var request = new BulkCancelWorkflowInstancesRequest - { - Ids = workflowInstanceIds - }; + var request = new BulkCancelWorkflowInstancesRequest { Ids = workflowInstanceIds }; await WorkflowInstanceService.BulkCancelAsync(request); } @@ -330,16 +416,30 @@ private async Task OnBulkCancelClicked() private Task OnImportClicked() { - return DomAccessor.ClickElementAsync("#instance-file-upload-button-wrapper input[type=file]"); + return DomAccessor.ClickElementAsync( + "#instance-file-upload-button-wrapper input[type=file]" + ); } private async Task OnFilesSelected(IReadOnlyList files) { var maxAllowedSize = 1024 * 1024 * 10; // 10 MB - var streamParts = files.Select(x => new StreamPart(x.OpenReadStream(maxAllowedSize), x.Name, x.ContentType)).ToList(); + var streamParts = files + .Select(x => new StreamPart(x.OpenReadStream(maxAllowedSize), x.Name, x.ContentType)) + .ToList(); var count = await WorkflowInstanceService.BulkImportAsync(streamParts); - var message = count == 1 ? Localizer["Successfully imported one instance"] : Localizer["Successfully imported {0} instances", count]; - Snackbar.Add(message, Severity.Success, options => { options.SnackbarVariant = Variant.Filled; }); + var message = + count == 1 + ? Localizer["Successfully imported one instance"] + : Localizer["Successfully imported {0} instances", count]; + Snackbar.Add( + message, + Severity.Success, + options => + { + options.SnackbarVariant = Variant.Filled; + } + ); Reload(); } @@ -351,7 +451,9 @@ private async Task OnBulkExportClicked() await Files.DownloadFileFromStreamAsync(fileName, download.Content); } - private async Task OnSelectedWorkflowDefinitionsChanged(IEnumerable values) + private async Task OnSelectedWorkflowDefinitionsChanged( + IEnumerable values + ) { SelectedWorkflowDefinitions = values.ToList(); await _table.ReloadServerData(); @@ -410,7 +512,12 @@ private async Task OnApplyTimestampFiltersClicked() private void StartElapsedTimer() { - _elapsedTimer ??= new(_ => InvokeAsync(async () => await _table.ReloadServerData()), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + _elapsedTimer ??= new( + _ => InvokeAsync(async () => await _table.ReloadServerData()), + null, + TimeSpan.Zero, + TimeSpan.FromSeconds(10) + ); } private void StopElapsedTimer() From 3749f1e1c7f5f639ef6a53b80cc1da7e17a02a49 Mon Sep 17 00:00:00 2001 From: Max Brooks Date: Fri, 6 Jun 2025 12:20:27 -0400 Subject: [PATCH 08/10] dev merge fix --- .../WorkflowInstanceList.razor.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs index 8f29d31a5..40092e0e1 100644 --- a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs @@ -64,24 +64,6 @@ public partial class WorkflowInstanceList : IAsyncDisposable [Inject] private ISnackbar Snackbar { get; set; } = default!; - [Inject] - private IWorkflowInstanceService WorkflowInstanceService { get; set; } = default!; - - [Inject] - private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = default!; - - [Inject] - private IBackendApiClientProvider BackendApiClientProvider { get; set; } = default!; - - [Inject] - private IFiles Files { get; set; } = default!; - - [Inject] - private IDomAccessor DomAccessor { get; set; } = default!; - - [Inject] - private ILogger Logger { get; set; } = default!; - private ICollection WorkflowDefinitions { get; set; } = new List(); private ICollection SelectedWorkflowDefinitions { get; set; } = From f64442e2d50dbe16ca0c8161f5759684bc5a0e10 Mon Sep 17 00:00:00 2001 From: Max Brooks <45081361+MaxBrooks114@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:16:57 -0400 Subject: [PATCH 09/10] Update Directory.Build.targets remove local targets --- Directory.Build.targets | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 941a125ca..8b1378917 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,8 +1 @@ - - - - - - - - \ No newline at end of file + From ca948c7599aafc4da1776f8f389efb495f33ef5c Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 6 Oct 2025 00:51:44 +0100 Subject: [PATCH 10/10] Delete Directory.Build.targets --- Directory.Build.targets | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Directory.Build.targets diff --git a/Directory.Build.targets b/Directory.Build.targets deleted file mode 100644 index 8b1378917..000000000 --- a/Directory.Build.targets +++ /dev/null @@ -1 +0,0 @@ -