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.Designer/ClientLib/src/designer/api/create-graph.ts b/src/modules/Elsa.Studio.Workflows.Designer/ClientLib/src/designer/api/create-graph.ts
index 09b6d97fe..a8fce835c 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,9 +135,10 @@ export async function createGraph(containerId: string, componentRef: DotNetCompo
rubberEdge: false,
rubberNode: true,
rubberband: true,
- movable: !readOnly,
+ movable: true,
showNodeSelectionBox: true,
className: 'elsa-selection'
+
}),
);
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 43a45b277..02dbef4a8 100644
--- a/src/modules/Elsa.Studio.Workflows.Designer/Components/FlowchartDesigner.razor.cs
+++ b/src/modules/Elsa.Studio.Workflows.Designer/Components/FlowchartDesigner.razor.cs
@@ -370,6 +370,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);
+ }
}
}
@@ -472,4 +476,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
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 afeb99bc7..40092e0e1 100644
--- a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs
+++ b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceList/WorkflowInstanceList.razor.cs
@@ -3,20 +3,23 @@
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.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 Microsoft.Extensions.Logging;
using MudBlazor;
using Refit;
-using Elsa.Api.Client.Resources.WorkflowInstances.Models;
-using Microsoft.Extensions.Logging;
namespace Elsa.Studio.Workflows.Components.WorkflowInstanceList;
@@ -31,19 +34,40 @@ 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 IUserMessageService UserMessageService { get; set; } = default!;
- [Inject] private IDialogService DialogService { get; set; } = default!;
- [Inject] private IUserMessageService UserMessageService { 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!;
+
+ [Inject]
+ private ISnackbar Snackbar { 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 +77,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 +110,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 +126,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 +177,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 };
@@ -146,7 +197,10 @@ private async Task> LoadData(TableState state, Ca
}
}
- private async Task> TryListWorkflowDefinitionsAsync(ListWorkflowInstancesRequest request, CancellationToken cancellationToken)
+ private async Task> TryListWorkflowDefinitionsAsync(
+ ListWorkflowInstancesRequest request,
+ CancellationToken cancellationToken
+ )
{
try
{
@@ -155,17 +209,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);
@@ -173,7 +228,7 @@ private TimestampFilter Map(TimestampFilterModel source)
{
Column = source.Column,
Operator = source.Operator,
- Timestamp = timestamp
+ Timestamp = timestamp,
};
}
@@ -185,7 +240,7 @@ private TimestampFilter Map(TimestampFilterModel source)
"Finished" => OrderByWorkflowInstance.Finished,
"Created" => OrderByWorkflowInstance.Created,
"LastExecuted" => OrderByWorkflowInstance.LastExecuted,
- _ => null
+ _ => null,
};
}
@@ -196,19 +251,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)
@@ -236,11 +293,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;
@@ -252,7 +316,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;
@@ -264,14 +333,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;
@@ -284,7 +361,9 @@ 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)
@@ -294,14 +373,11 @@ private async Task OnBulkCancelClicked()
if (applyToAllMatches)
{
- var cancel = new JsonObject
- {
- ["type"] = "Cancel"
- };
+ var cancel = new JsonObject { ["type"] = "Cancel" };
var plan = new AlterationPlanParams
{
- Alterations = [cancel],
+ Alterations = [],
Filter = new()
{
EmptyFilterSelectsAll = true,
@@ -310,22 +386,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);
- UserMessageService.ShowSnackbarTextMessage("Workflow instances are being cancelled.", Severity.Info, options => { options.SnackbarVariant = Variant.Filled; });
+ UserMessageService.ShowSnackbarTextMessage(
+ "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);
}
@@ -334,16 +424,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];
- UserMessageService.ShowSnackbarTextMessage(message, Severity.Success, options => { options.SnackbarVariant = Variant.Filled; });
+ var message =
+ count == 1
+ ? Localizer["Successfully imported one instance"]
+ : Localizer["Successfully imported {0} instances", count];
+ UserMessageService.ShowSnackbarTextMessage(
+ message,
+ Severity.Success,
+ options =>
+ {
+ options.SnackbarVariant = Variant.Filled;
+ }
+ );
Reload();
}
@@ -355,7 +459,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();
@@ -414,7 +520,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()
@@ -431,4 +542,4 @@ ValueTask IAsyncDisposable.DisposeAsync()
StopElapsedTimer();
return ValueTask.CompletedTask;
}
-}
\ 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 2b268ce94..0e49df262 100644
--- a/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs
+++ b/src/modules/Elsa.Studio.Workflows/Shared/Components/DiagramDesignerWrapper.razor.cs
@@ -147,52 +147,52 @@ public async Task SelectActivityAsync(string nodeId)
}
}
- private async Task SelectActivityAsync(JsonObject? activityToSelect, string? nodeId = null)
- {
- var targetNodeId = activityToSelect?.GetId() ?? nodeId;
- if (string.IsNullOrEmpty(targetNodeId))
- return;
- // Bail out if it's the same as last time
- if (targetNodeId == _lastSelectedNodeId)
- return;
+ private async Task SelectActivityAsync(JsonObject? activityToSelect, string? nodeId = null)
+ {
+ var targetNodeId = activityToSelect?.GetId() ?? nodeId;
+ if (string.IsNullOrEmpty(targetNodeId))
+ return;
- // Remember for the next call
- _lastSelectedNodeId = targetNodeId;
+ // Bail out if it's the same as last time
+ if (targetNodeId == _lastSelectedNodeId)
+ return;
- if (nodeId == null)
- return;
+ // Remember for the next call
+ _lastSelectedNodeId = targetNodeId;
- // Load the selected node path from the backend.
- var pathSegmentsResponse = await WorkflowDefinitionService.GetPathSegmentsAsync(
- WorkflowDefinitionVersionId,
- nodeId
- );
- if (pathSegmentsResponse == null)
- return;
+ if (nodeId == null)
+ return;
- await IndexActivityNodes(pathSegmentsResponse.Container.Activity);
-
- activityToSelect = pathSegmentsResponse.ChildNode.Activity;
- var pathSegments = pathSegmentsResponse.PathSegments.ToList();
- StateHasChanged();
+ // Load the selected node path from the backend.
+ var pathSegmentsResponse = await WorkflowDefinitionService.GetPathSegmentsAsync(
+ WorkflowDefinitionVersionId,
+ nodeId
+ );
- // Reassign the current path.
- await UpdatePathSegmentsAsync(segments =>
- {
- segments.Clear();
+ if (pathSegmentsResponse == null)
+ return;
- foreach (var segment in pathSegments)
- segments.Push(segment);
- });
-
- // Display the new segment.
- _currentContainerActivity = null;
- await DisplayCurrentSegmentAsync();
+ activityToSelect = pathSegmentsResponse.ChildNode.Activity;
+ var pathSegments = pathSegmentsResponse.PathSegments.ToList();
+ StateHasChanged();
- // Select the activity.
- await _diagramDesigner!.SelectActivityAsync(activityToSelect.GetId());
- }
+ // Reassign the current path.
+ await UpdatePathSegmentsAsync(segments =>
+ {
+ segments.Clear();
+
+ foreach (var segment in pathSegments)
+ segments.Push(segment);
+ });
+
+ // Display the new segment.
+ _currentContainerActivity = null;
+ await DisplayCurrentSegmentAsync();
+
+ // Select the activity.
+ await _diagramDesigner!.SelectActivityAsync(activityToSelect.GetId());
+ }
/// Updates the stats of the specified activity.
/// The ID of the activity to update.
@@ -469,7 +469,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.
@@ -485,101 +485,115 @@ private async Task OnActivityUpdated(JsonObject activity)
await ActivityUpdated.InvokeAsync(activity);
}
- private async Task OnActivityEmbeddedPortSelected(ActivityEmbeddedPortSelectedArgs args)
- {
- var nodes = _indexedActivityNodes;
- var selectedActivity = args.Activity;
- var activity = nodes.TryGetValue(selectedActivity.GetNodeId(), out var selectedActivityNode)
- ? selectedActivityNode.Activity
- : null;
-
- if (activity is null)
- return;
-
- var portName = args.PortName;
- var activityTypeName = activity.GetTypeName();
- var activityVersion = activity.GetVersion();
- var activityDescriptor = ActivityRegistry.Find(activityTypeName, activityVersion)!;
- var portProviderContext = new PortProviderContext(activityDescriptor, activity);
- var portProvider = ActivityPortService.GetProvider(portProviderContext);
- var embeddedActivity = portProvider.ResolvePort(portName, portProviderContext);
-
- if (embeddedActivity == null)
- {
- // Lazy load.
- 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 propName = portName.Camelize();
- var selectedPortActivity = (JsonObject)selectedActivityGraph.Activity[propName]!;
- embeddedActivity = selectedPortActivity;
- portProvider.AssignPort(args.PortName, embeddedActivity, new(activityDescriptor, activity));
- await IndexActivityNodes(selectedActivityGraph.Activity);
- }
- }
-
- 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"
- )
- {
- if (ActivitySelected.HasDelegate)
- await ActivitySelected.InvokeAsync(embeddedActivity);
- return;
- }
+ private async Task OnActivityEmbeddedPortSelected(ActivityEmbeddedPortSelectedArgs args)
+ {
+ var nodes = _indexedActivityNodes;
+ var selectedActivity = args.Activity;
+ var activity = nodes.TryGetValue(selectedActivity.GetNodeId(), out var selectedActivityNode)
+ ? selectedActivityNode.Activity
+ : null;
+
+ if (activity is null)
+ return;
+
+ var portName = args.PortName;
+ var activityTypeName = activity.GetTypeName();
+ var activityVersion = activity.GetVersion();
+ var activityDescriptor = ActivityRegistry.Find(activityTypeName, activityVersion)!;
+ var portProviderContext = new PortProviderContext(activityDescriptor, activity);
+ var portProvider = ActivityPortService.GetProvider(portProviderContext);
+ var embeddedActivity = portProvider.ResolvePort(portName, portProviderContext);
+
+ if (embeddedActivity == null)
+ {
+ // Lazy load.
+ 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 propName = portName.Camelize();
+ var selectedPortActivity = (JsonObject)selectedActivityGraph.Activity[propName]!;
+ embeddedActivity = selectedPortActivity;
+ portProvider.AssignPort(
+ args.PortName,
+ embeddedActivity,
+ new PortProviderContext(activityDescriptor, activity)
+ );
+ await IndexActivityNodes(selectedActivityGraph.Activity);
+ }
+ }
+
+ if (embeddedActivity != null)
+ {
+ 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;
+ }
+ }
+ else
+ {
+ if (!IsReadOnly)
+ {
+ 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)
+ );
+
+ // Update the graph in the designer.
+ await _diagramDesigner!.UpdateActivityAsync(activity.GetId(), activity);
+ }
+ else
+ {
+ return;
+ }
+ }
+
+ // 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
+ );
+
+ await UpdatePathSegmentsAsync(segments => segments.Push(segment));
+ await DisplayCurrentSegmentAsync();
+ }
- // If the embedded activity type is a flowchart or workflow, we can display it in the designer.
- }
- else
- {
- if (!IsReadOnly)
- {
- var embeddedActivityId = IdentityGenerator.GenerateId();
- // Create a flowchart and embed it into the activity.
- embeddedActivity = new(new Dictionary
- {
- ["id"] = embeddedActivityId,
- ["nodeId"] = $"{activity.GetNodeId()}:{embeddedActivityId}",
- ["type"] = "Elsa.Flowchart",
- ["version"] = 1,
- ["name"] = "Flowchart1",
- });
-
- portProvider.AssignPort(args.PortName, embeddedActivity, new(activityDescriptor, activity));
-
- // Update the graph in the designer.
- await _diagramDesigner!.UpdateActivityAsync(activity.GetId(), activity);
- }
- else
- {
- return;
- }
- }
-
- // 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
- );
-
- await UpdatePathSegmentsAsync(segments => segments.Push(segment));
- await DisplayCurrentSegmentAsync();
- }
private async Task OnGraphUpdated()
{