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