diff --git a/src/designer/elsa-workflows-designer/src/components/toolbar/new-button/new-button.tsx b/src/designer/elsa-workflows-designer/src/components/toolbar/new-button/new-button.tsx index 30dfa1a39..1a1a84d5a 100644 --- a/src/designer/elsa-workflows-designer/src/components/toolbar/new-button/new-button.tsx +++ b/src/designer/elsa-workflows-designer/src/components/toolbar/new-button/new-button.tsx @@ -1,7 +1,7 @@ import {Component, Event, EventEmitter, Host, h, Listen, Prop} from '@stencil/core'; import {leave, toggle} from 'el-transition' import newButtonItemStore from "../../../data/new-button-item-store"; -import {MenuItem} from "../../shared/context-menu/models"; +import {MenuItem, MenuItemGroup} from "../../shared/context-menu/models"; @Component({ tag: 'elsa-new-button', @@ -30,12 +30,6 @@ export class NewButton { toggle(this.menu); } - private onNewWorkflowDefinitionClick(e: Event) { - e.preventDefault(); - this.newClicked.emit(); - leave(this.menu); - } - private onItemClick = (e: MouseEvent, item: MenuItem) => { e.preventDefault(); @@ -47,7 +41,7 @@ export class NewButton { render() { - const items: Array = newButtonItemStore.items; + const items: Array = newButtonItemStore.items; const mainItem: MenuItem = newButtonItemStore.mainItem ?? { text: 'New', @@ -79,16 +73,21 @@ export class NewButton { class="hidden origin-bottom-right absolute right-0 top-10 mb-2 -mr-1 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5"> diff --git a/src/designer/elsa-workflows-designer/src/modules/activity-definitions/plugin.tsx b/src/designer/elsa-workflows-designer/src/modules/activity-definitions/plugin.tsx index 15931ab50..9711e3693 100644 --- a/src/designer/elsa-workflows-designer/src/modules/activity-definitions/plugin.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/activity-definitions/plugin.tsx @@ -3,7 +3,7 @@ import {h} from "@stencil/core"; import {Container, Service} from "typedi"; import {ActivityDescriptor, Plugin} from "../../models"; import newButtonItemStore from "../../data/new-button-item-store"; -import {MenuItem} from "../../components/shared/context-menu/models"; +import {MenuItem, MenuItemGroup} from "../../components/shared/context-menu/models"; import {Flowchart} from "../flowchart/models"; import {generateUniqueActivityName} from '../../utils/generate-activity-name'; import descriptorsStore from "../../data/descriptors-store"; @@ -43,13 +43,17 @@ export class ActivityDefinitionsPlugin implements Plugin { clickHandler: this.onNewActivityDefinitionClick } + const newItemGroup: MenuItemGroup = { + menuItems: [newActivityDefinitionItem] + }; + const activityDefinitionBrowserItem: ToolbarMenuItem = { text: 'Activity Definitions', onClick: this.onBrowseActivityDefinitions, order: 5 }; - newButtonItemStore.items = [...newButtonItemStore.items, newActivityDefinitionItem]; + newButtonItemStore.items = [...newButtonItemStore.items, newItemGroup]; toolbarButtonMenuItemStore.items = [...toolbarButtonMenuItemStore.items, activityDefinitionBrowserItem]; } diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/publish-button.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/publish-button.tsx index d10a1407e..317bd2028 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/publish-button.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/publish-button.tsx @@ -22,7 +22,6 @@ export class PublishButton { @Event({bubbles: true}) importClicked: EventEmitter; menu: HTMLElement; - fileInput: HTMLInputElement; element: HTMLElement; @Listen('click', {target: 'window'}) @@ -70,22 +69,10 @@ export class PublishButton { private async onImportClick(e: Event) { e.preventDefault(); - this.fileInput.value = null; - this.fileInput.click(); - + this.importClicked.emit(); leave(this.menu); } - private async onFileInputChange(e: Event) { - const files = this.fileInput.files; - - if (files.length == 0) { - return; - } - - this.importClicked.emit(files[0]); - } - render() { const newMenuItems: Array = newButtonItemStore.items; @@ -141,7 +128,6 @@ export class PublishButton { - this.onFileInputChange(e)} ref={el => this.fileInput = el}/> ); } diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/plugin.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/plugin.tsx index 800643a21..e8f40bfda 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/plugin.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/plugin.tsx @@ -3,7 +3,7 @@ import {h} from "@stencil/core"; import {Container, Service} from "typedi"; import {ActivityDescriptor, Plugin} from "../../models"; import newButtonItemStore from "../../data/new-button-item-store"; -import {MenuItem} from "../../components/shared/context-menu/models"; +import {MenuItem, MenuItemGroup} from "../../components/shared/context-menu/models"; import {Flowchart} from "../flowchart/models"; import {generateUniqueActivityName} from '../../utils/generate-activity-name'; import descriptorsStore from "../../data/descriptors-store"; @@ -17,9 +17,10 @@ import {WorkflowDefinitionManager} from "./services/manager"; import {WorkflowDefinition, WorkflowDefinitionSummary} from "./models/entities"; import {WorkflowDefinitionUpdatedArgs} from "./models/ui"; import {PublishClickedArgs} from "./components/publish-button"; -import {WorkflowDefinitionsApi} from "./services/api"; +import {ExportWorkflowRequest, ImportWorkflowRequest, WorkflowDefinitionsApi} from "./services/api"; import {DefaultModalActions, ModalDialogInstance, ModalDialogService} from "../../components/shared/modal-dialog"; import {isEqual} from 'lodash' +import {htmlToElement} from "../../utils"; const FlowchartTypeName = 'Elsa.Flowchart'; @@ -43,13 +44,22 @@ export class WorkflowDefinitionsPlugin implements Plugin { clickHandler: this.onNewWorkflowDefinitionClick } + const importWorkflowDefinitionItem: MenuItem = { + text: 'Import', + clickHandler: this.onImportWorkflowDefinitionClick + } + + const newItemGroup: MenuItemGroup = { + menuItems: [newWorkflowDefinitionItem, importWorkflowDefinitionItem] + } + const workflowDefinitionBrowserItem: ToolbarMenuItem = { text: 'Workflow Definitions', onClick: this.onBrowseWorkflowDefinitions, order: 5 }; - newButtonItemStore.items = [...newButtonItemStore.items, newWorkflowDefinitionItem]; + newButtonItemStore.items = [...newButtonItemStore.items, newItemGroup]; toolbarButtonMenuItemStore.items = [...toolbarButtonMenuItemStore.items, workflowDefinitionBrowserItem]; } @@ -98,15 +108,41 @@ export class WorkflowDefinitionsPlugin implements Plugin { } private showWorkflowDefinitionEditor = (workflowDefinition: WorkflowDefinition) => { - toolbarComponentStore.components = [() => ]; + toolbarComponentStore.components = [() => ]; studioComponentStore.activeComponentFactory = () => this.workflowDefinitionEditorElement = el}/>; }; + private import = async () => { + const fileInput = htmlToElement(''); + + document.body.append(fileInput); + fileInput.click(); + + fileInput.addEventListener('change', async e => { + const files = fileInput.files; + + if(files.length == 0) { + fileInput.remove(); + return; + } + + const file = files[0]; + const importedWorkflow = await this.workflowDefinitionManager.importWorkflow(null, file); + fileInput.remove(); + this.showWorkflowDefinitionEditor(importedWorkflow); + }); + }; + private onNewWorkflowDefinitionClick = async () => { await this.newWorkflow(); this.modalDialogService.hide(this.workflowDefinitionBrowserInstance); }; + private onImportWorkflowDefinitionClick = async () => { + await this.import(); + this.modalDialogService.hide(this.workflowDefinitionBrowserInstance); + }; + private onWorkflowUpdated = async (e: CustomEvent) => { const updatedWorkflowDefinition = e.detail.workflowDefinition; @@ -151,4 +187,13 @@ export class WorkflowDefinitionsPlugin implements Plugin { await this.eventBus.emit(NotificationEventTypes.Update, this, {id: workflowDefinition.definitionId, message: `${workflowDefinition.name} publish finished`}); e.detail.complete(); } + + private onExportClicked = async (e: CustomEvent) => { + const workflowDefinition = await this.workflowDefinitionEditorElement.getWorkflowDefinition(); + await this.workflowDefinitionManager.exportWorkflow(workflowDefinition); + } + + private onImportClicked = async (e: CustomEvent) => { + await this.import(); + } } diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/services/api.ts b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/services/api.ts index 64fb3da79..f7965e89f 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/services/api.ts +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/services/api.ts @@ -1,8 +1,9 @@ -import { getVersionOptionsString, serializeQueryString } from '../../../utils'; +import {getVersionOptionsString, serializeQueryString} from '../../../utils'; import {WorkflowDefinition, WorkflowDefinitionSummary} from "../models/entities"; import {Activity, PagedList, Variable, VersionedEntity, VersionOptions} from "../../../models"; import {Service} from "typedi"; import {ElsaApiClientProvider} from "../../../services"; +import {AxiosResponse} from "axios"; @Service() export class WorkflowDefinitionsApi { @@ -114,15 +115,24 @@ export class WorkflowDefinitionsApi { const definitionId = request.definitionId; const json = await file.text(); const httpClient = await this.getHttpClient(); + let response: AxiosResponse; - const response = await httpClient.post(`workflow-definitions/${definitionId}/import`, json, { - headers: { - 'Content-Type': 'application/json', - }, - }); + if (!definitionId) { + response = await httpClient.put(`workflow-definitions/import`, json, { + headers: { + 'Content-Type': 'application/json', + }, + }); + } else { + response = await httpClient.post(`workflow-definitions/${definitionId}/import`, json, { + headers: { + 'Content-Type': 'application/json', + }, + }); + } const workflowDefinition = response.data; - return { workflowDefinition: workflowDefinition }; + return {workflowDefinition: workflowDefinition}; } async deleteMany(request: DeleteManyWorkflowDefinitionRequest): Promise { @@ -166,9 +176,15 @@ export interface SaveWorkflowDefinitionRequest { export interface BaseManyWorkflowDefinitionRequest { definitionIds: string[]; } -export interface DeleteManyWorkflowDefinitionRequest extends BaseManyWorkflowDefinitionRequest {} -export interface PublishManyWorkflowDefinitionRequest extends BaseManyWorkflowDefinitionRequest {} -export interface UnpublishManyWorkflowDefinitionRequest extends BaseManyWorkflowDefinitionRequest {} + +export interface DeleteManyWorkflowDefinitionRequest extends BaseManyWorkflowDefinitionRequest { +} + +export interface PublishManyWorkflowDefinitionRequest extends BaseManyWorkflowDefinitionRequest { +} + +export interface UnpublishManyWorkflowDefinitionRequest extends BaseManyWorkflowDefinitionRequest { +} export interface DeleteWorkflowDefinitionRequest { definitionId: string; @@ -208,7 +224,7 @@ export interface ExportWorkflowResponse { } export interface ImportWorkflowRequest { - definitionId: string; + definitionId?: string; file: File; } @@ -220,6 +236,7 @@ export enum WorkflowDefinitionsOrderBy { Name = 'Name', CreatedAt = 'CreatedAt', } + export interface ListWorkflowDefinitionsRequest { page?: number; pageSize?: number; @@ -239,6 +256,7 @@ export interface PublishManyWorkflowDefinitionResponse { alreadyPublished: string[]; notFound: string[]; } + export interface UnpublishManyWorkflowDefinitionResponse { retracted: string[]; notPublished: string[]; diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/services/manager.ts b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/services/manager.ts index 9e2859b80..dc6ba1f5e 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/services/manager.ts +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/services/manager.ts @@ -1,6 +1,7 @@ import {Container, Service} from "typedi"; import {WorkflowDefinition} from "../models/entities"; -import {RetractWorkflowDefinitionRequest, SaveWorkflowDefinitionRequest, WorkflowDefinitionsApi} from "./api"; +import {ExportWorkflowRequest, ImportWorkflowRequest, RetractWorkflowDefinitionRequest, SaveWorkflowDefinitionRequest, WorkflowDefinitionsApi} from "./api"; +import {downloadFromBlob} from "../../../utils"; @Service() export class WorkflowDefinitionManager { @@ -22,13 +23,34 @@ export class WorkflowDefinitionManager { }; return await this.api.post(request); - } + }; - public retractWorkflow = async (workflow: WorkflowDefinition): Promise => { + public retractWorkflow = async (definition: WorkflowDefinition): Promise => { const request: RetractWorkflowDefinitionRequest = { - definitionId: workflow.definitionId + definitionId: definition.definitionId }; return await this.api.retract(request); - } + }; + + public exportWorkflow = async (definition: WorkflowDefinition): Promise => { + const request: ExportWorkflowRequest = { + definitionId: definition.definitionId, + versionOptions: {version: definition.version} + }; + + const response = await this.api.export(request); + downloadFromBlob(response.data, {contentType: 'application/json', fileName: response.fileName}); + }; + + public importWorkflow = async (definitionId: string, file: File): Promise => { + try { + const importRequest: ImportWorkflowRequest = {definitionId, file}; + const importResponse = await this.api.import(importRequest); + return importResponse.workflowDefinition; + } catch (e) { + console.error(e); + } + }; + } diff --git a/src/designer/elsa-workflows-designer/src/utils/utils.ts b/src/designer/elsa-workflows-designer/src/utils/utils.ts index 18b8c6fa3..a6feb7c6b 100644 --- a/src/designer/elsa-workflows-designer/src/utils/utils.ts +++ b/src/designer/elsa-workflows-designer/src/utils/utils.ts @@ -108,3 +108,10 @@ export function durationToString(duration: moment.Duration) { : `${duration.asMilliseconds()} ms` : null; } + +export function htmlToElement(html: string): TElement { + const template = document.createElement('template'); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + return template.content.firstChild as unknown as TElement; +} diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs index 74cb648b0..1ea046692 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs @@ -4,11 +4,16 @@ using Elsa.Workflows.Api.Models; using Elsa.Workflows.Core.Serialization; using Elsa.Workflows.Management.Mappers; using Elsa.Workflows.Management.Materializers; +using Elsa.Workflows.Management.Models; using Elsa.Workflows.Management.Services; using Elsa.Workflows.Runtime.Services; +using FastEndpoints; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import; +/// +/// Imports a JSON file containing a workflow definition. +/// public class Import : ElsaEndpoint { private readonly SerializerOptionsProvider _serializerOptionsProvider; @@ -16,6 +21,7 @@ public class Import : ElsaEndpoint public Import( SerializerOptionsProvider serializerOptionsProvider, IWorkflowDefinitionPublisher workflowDefinitionPublisher, @@ -28,12 +34,15 @@ public class Import : ElsaEndpoint public override void Configure() { - Post("workflow-definitions/import", "workflow-definitions/{definitionId}/import"); + Routes("workflow-definitions/import", "workflow-definitions/{definitionId}/import"); + Verbs(Http.POST, Http.PUT); ConfigurePermissions("write:workflow-definitions"); } + /// public override async Task HandleAsync(WorkflowDefinitionRequest request, CancellationToken cancellationToken) { var definitionId = request.DefinitionId; @@ -64,9 +73,9 @@ public class Import : ElsaEndpoint(); draft.Variables = variables; - draft.ApplicationProperties = request.ApplicationProperties; + draft.ApplicationProperties = request.ApplicationProperties ?? new Dictionary(); draft = request.Publish ? await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken) : await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken); // Materialize the workflow definition for serialization. @@ -79,7 +88,7 @@ public class Import : ElsaEndpoint(), draft.Metadata, draft.ApplicationProperties, draft.IsLatest,