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() {