diff --git a/src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj b/src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj index d72b4adf..0ec7b44c 100644 --- a/src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj +++ b/src/framework/Elsa.Studio.Core/Elsa.Studio.Core.csproj @@ -25,12 +25,12 @@ - - - + + + - + diff --git a/src/modules/Elsa.Studio.Workflows.Core/Domain/Contracts/IWorkflowDefinitionService.cs b/src/modules/Elsa.Studio.Workflows.Core/Domain/Contracts/IWorkflowDefinitionService.cs index 1664ccd9..d22555f8 100644 --- a/src/modules/Elsa.Studio.Workflows.Core/Domain/Contracts/IWorkflowDefinitionService.cs +++ b/src/modules/Elsa.Studio.Workflows.Core/Domain/Contracts/IWorkflowDefinitionService.cs @@ -4,6 +4,7 @@ using Elsa.Api.Client.Shared.Models; using Elsa.Studio.Models; using Elsa.Studio.Workflows.Domain.Models; +using Refit; namespace Elsa.Studio.Workflows.Domain.Contracts; @@ -102,6 +103,11 @@ public interface IWorkflowDefinitionService /// Task ImportDefinitionAsync(WorkflowDefinitionModel definitionModel, CancellationToken cancellationToken = default); + /// + /// Exports a set of workflow definitions. + /// + Task BulkExportDefinitionsAsync(IEnumerable ids, CancellationToken cancellationToken = default); + /// /// Updates the references of a workflow definition. /// @@ -116,4 +122,9 @@ public interface IWorkflowDefinitionService /// Executes a workflow definition. /// Task ExecuteAsync(string definitionId, ExecuteWorkflowDefinitionRequest? request, CancellationToken cancellationToken = default); + + /// + /// Imports a set of files containing workflow definitions. + /// + Task ImportFilesAsync(IEnumerable streamParts, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/Elsa.Studio.Workflows.Core/Domain/Models/FileDownload.cs b/src/modules/Elsa.Studio.Workflows.Core/Domain/Models/FileDownload.cs index 20748646..58beffbb 100644 --- a/src/modules/Elsa.Studio.Workflows.Core/Domain/Models/FileDownload.cs +++ b/src/modules/Elsa.Studio.Workflows.Core/Domain/Models/FileDownload.cs @@ -3,4 +3,4 @@ namespace Elsa.Studio.Workflows.Domain.Models; /// /// Represents a file download. /// -public record FileDownload(string? FileName, Stream Content); \ No newline at end of file +public record FileDownload(string FileName, Stream Content); \ No newline at end of file diff --git a/src/modules/Elsa.Studio.Workflows.Core/Domain/Services/RemoteWorkflowDefinitionService.cs b/src/modules/Elsa.Studio.Workflows.Core/Domain/Services/RemoteWorkflowDefinitionService.cs index ad142308..f83645c4 100644 --- a/src/modules/Elsa.Studio.Workflows.Core/Domain/Services/RemoteWorkflowDefinitionService.cs +++ b/src/modules/Elsa.Studio.Workflows.Core/Domain/Services/RemoteWorkflowDefinitionService.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.Json.Nodes; +using Elsa.Api.Client.Extensions; using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts; using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; using Elsa.Api.Client.Resources.WorkflowDefinitions.Requests; @@ -249,15 +250,7 @@ public async Task ExportDefinitionAsync(string definitionId, Versi { var api = await GetApiAsync(cancellationToken); var response = await api.ExportAsync(definitionId, versionOptions, cancellationToken); - var fileName = $"workflow-definition-{definitionId}.json"; - - if (response.Headers.TryGetValues("content-disposition", out var contentDispositionHeader)) // Only available if the Elsa Server exposes the "Content-Disposition" header. - { - var values = contentDispositionHeader?.ToList() ?? new List(); - - if (values.Count >= 2) - fileName = values[1].Split('=')[1]; - } + var fileName = response.GetDownloadedFileNameOrDefault($"workflow-definition-{definitionId}.json"); return new FileDownload(fileName, response.Content!); } @@ -269,6 +262,16 @@ public async Task ImportDefinitionAsync(WorkflowDefinitionMo return await api.ImportAsync(definitionModel, cancellationToken); } + /// + public async Task BulkExportDefinitionsAsync(IEnumerable ids, CancellationToken cancellationToken = default) + { + var api = await GetApiAsync(cancellationToken); + var request = new BulkExportWorkflowDefinitionsRequest(ids.ToArray()); + var response = await api.BulkExportAsync(request, cancellationToken); + var fileName = response.GetDownloadedFileNameOrDefault("workflow-definitions.zip"); + return new FileDownload(fileName, response.Content!); + } + /// public async Task UpdateReferencesAsync(string definitionId, CancellationToken cancellationToken = default) { @@ -288,10 +291,17 @@ public async Task ExecuteAsync(string definitionId, ExecuteWorkflowDefin { var api = await GetApiAsync(cancellationToken); var response = await api.ExecuteAsync(definitionId, request, cancellationToken); - var workflowInstanceId = response.Headers.GetValues("x-elsa-workflow-instance-id").First(); return workflowInstanceId; } + /// + public async Task ImportFilesAsync(IEnumerable streamParts, CancellationToken cancellationToken = default) + { + var api = await GetApiAsync(cancellationToken); + var response = await api.ImportFilesAsync(streamParts.ToList(), cancellationToken); + return response.Count; + } + private async Task GetApiAsync(CancellationToken cancellationToken = default) => await _remoteBackendApiClientProvider.GetApiAsync(cancellationToken); } \ No newline at end of file diff --git a/src/modules/Elsa.Studio.Workflows/Components/WorkflowDefinitionEditor/Components/WorkflowEditor.razor b/src/modules/Elsa.Studio.Workflows/Components/WorkflowDefinitionEditor/Components/WorkflowEditor.razor index a5647a96..16fa1baf 100644 --- a/src/modules/Elsa.Studio.Workflows/Components/WorkflowDefinitionEditor/Components/WorkflowEditor.razor +++ b/src/modules/Elsa.Studio.Workflows/Components/WorkflowDefinitionEditor/Components/WorkflowEditor.razor @@ -5,44 +5,44 @@ - + - + - +
- +
- + @if (WorkflowDefinition?.IsPublished == true) { - Unpublish + Unpublish } - Export + Export - Import + Import - +
- +
\ No newline at end of file diff --git a/src/modules/Elsa.Studio.Workflows/Pages/WorkflowDefinitions/List/Index.razor b/src/modules/Elsa.Studio.Workflows/Pages/WorkflowDefinitions/List/Index.razor index e1728d92..87cf4d24 100644 --- a/src/modules/Elsa.Studio.Workflows/Pages/WorkflowDefinitions/List/Index.razor +++ b/src/modules/Elsa.Studio.Workflows/Pages/WorkflowDefinitions/List/Index.razor @@ -6,6 +6,10 @@ + +
+ +
- Delete - Publish - Unpublish + Delete + Publish + Unpublish + Export - Create workflow + + + Create workflow + + + Import + + + + diff --git a/src/modules/Elsa.Studio.Workflows/Pages/WorkflowDefinitions/List/Index.razor.cs b/src/modules/Elsa.Studio.Workflows/Pages/WorkflowDefinitions/List/Index.razor.cs index 47b53b39..4b1be77e 100644 --- a/src/modules/Elsa.Studio.Workflows/Pages/WorkflowDefinitions/List/Index.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Pages/WorkflowDefinitions/List/Index.razor.cs @@ -1,3 +1,7 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Elsa.Api.Client.Converters; +using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; using Elsa.Api.Client.Resources.WorkflowDefinitions.Responses; using Elsa.Api.Client.Shared.Models; using Elsa.Studio.DomInterop.Contracts; @@ -5,7 +9,9 @@ using Elsa.Studio.Workflows.Models; using Humanizer; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; using MudBlazor; +using Refit; namespace Elsa.Studio.Workflows.Pages.WorkflowDefinitions.List; @@ -24,6 +30,7 @@ public partial class Index [Inject] private ISnackbar Snackbar { get; set; } = default!; [Inject] private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = default!; [Inject] private IFiles Files { get; set; } = default!; + [Inject] private IDomAccessor DomAccessor { get; set; } = default!; private async Task> ServerReload(TableState state) { @@ -53,7 +60,7 @@ private async Task> ServerReload(TableState sta : publishedWorkflowDefinitions.Items.FirstOrDefault(x => x.DefinitionId == definition.DefinitionId); var publishedVersionNumber = publishedVersion?.Version; - return new WorkflowDefinitionRow(definition.DefinitionId, latestVersionNumber, publishedVersionNumber, definition.Name, definition.Description, definition.IsPublished); + return new WorkflowDefinitionRow(definition.Id, definition.DefinitionId, latestVersionNumber, publishedVersionNumber, definition.Name, definition.Description, definition.IsPublished); }) .ToList(); @@ -95,7 +102,7 @@ private void Edit(string definitionId) { NavigationManager.NavigateTo($"workflows/definitions/{definitionId}/edit"); } - + private void Reload() { _table.ReloadServerData(); @@ -173,7 +180,7 @@ private async Task OnBulkPublishClicked() Reload(); } - + private async Task OnBulkRetractClicked() { var result = await DialogService.ShowMessageBox("Unpublish selected workflows?", "Are you sure you want to unpublish the selected workflows?", yesText: "Unpublish", cancelText: "Cancel"); @@ -205,6 +212,28 @@ private async Task OnBulkRetractClicked() Reload(); } + private async Task OnBulkExportClicked() + { + var workflowVersionIds = _selectedRows.Select(x => x.Id).ToList(); + var download = await WorkflowDefinitionService.BulkExportDefinitionsAsync(workflowVersionIds); + var fileName = download.FileName; + await Files.DownloadFileFromStreamAsync(fileName, download.Content); + } + + private Task OnImportClicked() + { + return DomAccessor.ClickElementAsync("#workflow-file-upload-button-wrapper input[type=file]"); + } + + private async Task OnFilesSelected(IReadOnlyList files) + { + var streamParts = files.Select(x => new StreamPart(x.OpenReadStream(), x.Name, x.ContentType)).ToList(); + var count = await WorkflowDefinitionService.ImportFilesAsync(streamParts); + var message = count == 1 ? "Successfully imported one workflow" : $"Successfully imported {count} workflows"; + Snackbar.Add(message, Severity.Success, options => { options.SnackbarVariant = Variant.Filled; }); + Reload(); + } + private void OnSearch(string text) { _searchString = text; @@ -222,6 +251,6 @@ private async Task OnRetractClicked(string definitionId) await WorkflowDefinitionService.RetractAsync(definitionId); Snackbar.Add("Workflow retracted", Severity.Success, options => { options.SnackbarVariant = Variant.Filled; }); } - - private record WorkflowDefinitionRow(string DefinitionId, int LatestVersion, int? PublishedVersion, string? Name, string? Description, bool IsPublished); + + private record WorkflowDefinitionRow(string Id, string DefinitionId, int LatestVersion, int? PublishedVersion, string? Name, string? Description, bool IsPublished); } \ No newline at end of file