Update import / export (#3471)

This commit is contained in:
Sipke Schoorstra 2022-11-25 13:27:35 +01:00 committed by GitHub
parent ae1590220d
commit 6bab9982cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 58 deletions

View file

@ -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<MenuItem> = newButtonItemStore.items;
const items: Array<MenuItemGroup> = 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">
<div class="divide-y divide-gray-100 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="option-menu">
{items.map(groupItem => {
return <div class="py-1" role="none">
{groupItem.menuItems.map(item => {
return (
<div class="py-1" role="none">
<a href="#" onClick={e => this.onItemClick(e, item)}
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
role="menuitem">
{item.text}
</a>
</div>);
})}
{items.map(item => (
<div class="py-1" role="none">
<a href="#" onClick={e => this.onItemClick(e, item)}
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900"
role="menuitem">
{item.text}
</a>
</div>))}
</div>
})}
</div>
</div>
</span>

View file

@ -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];
}

View file

@ -22,7 +22,6 @@ export class PublishButton {
@Event({bubbles: true}) importClicked: EventEmitter<File>;
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<MenuItem> = newButtonItemStore.items;
@ -141,7 +128,6 @@ export class PublishButton {
</div>
</span>
</span>
<input type="file" class="hidden" onChange={e => this.onFileInputChange(e)} ref={el => this.fileInput = el}/>
</Host>
);
}

View file

@ -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 = [() => <elsa-workflow-publish-button onPublishClicked={this.onPublishClicked}/>];
toolbarComponentStore.components = [() => <elsa-workflow-publish-button onPublishClicked={this.onPublishClicked} onExportClicked={this.onExportClicked} onImportClicked={this.onImportClicked}/>];
studioComponentStore.activeComponentFactory = () => <elsa-workflow-definition-editor workflowDefinition={workflowDefinition} onWorkflowUpdated={this.onWorkflowUpdated} ref={el => this.workflowDefinitionEditorElement = el}/>;
};
private import = async () => {
const fileInput = htmlToElement<HTMLInputElement>('<input type="file" />');
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<WorkflowDefinitionUpdatedArgs>) => {
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();
}
}

View file

@ -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<WorkflowDefinition>(`workflow-definitions/${definitionId}/import`, json, {
headers: {
'Content-Type': 'application/json',
},
});
if (!definitionId) {
response = await httpClient.put<WorkflowDefinition>(`workflow-definitions/import`, json, {
headers: {
'Content-Type': 'application/json',
},
});
} else {
response = await httpClient.post<WorkflowDefinition>(`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<DeleteManyWorkflowDefinitionResponse> {
@ -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[];

View file

@ -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<WorkflowDefinition> => {
public retractWorkflow = async (definition: WorkflowDefinition): Promise<WorkflowDefinition> => {
const request: RetractWorkflowDefinitionRequest = {
definitionId: workflow.definitionId
definitionId: definition.definitionId
};
return await this.api.retract(request);
}
};
public exportWorkflow = async (definition: WorkflowDefinition): Promise<void> => {
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<WorkflowDefinition> => {
try {
const importRequest: ImportWorkflowRequest = {definitionId, file};
const importResponse = await this.api.import(importRequest);
return importResponse.workflowDefinition;
} catch (e) {
console.error(e);
}
};
}

View file

@ -108,3 +108,10 @@ export function durationToString(duration: moment.Duration) {
: `${duration.asMilliseconds()} ms`
: null;
}
export function htmlToElement<TElement>(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;
}

View file

@ -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;
/// <summary>
/// Imports a JSON file containing a workflow definition.
/// </summary>
public class Import : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinitionResponse>
{
private readonly SerializerOptionsProvider _serializerOptionsProvider;
@ -16,6 +21,7 @@ public class Import : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinition
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly VariableDefinitionMapper _variableDefinitionMapper;
/// <inheritdoc />
public Import(
SerializerOptionsProvider serializerOptionsProvider,
IWorkflowDefinitionPublisher workflowDefinitionPublisher,
@ -28,12 +34,15 @@ public class Import : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinition
_variableDefinitionMapper = variableDefinitionMapper;
}
/// <inheritdoc />
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");
}
/// <inheritdoc />
public override async Task HandleAsync(WorkflowDefinitionRequest request, CancellationToken cancellationToken)
{
var definitionId = request.DefinitionId;
@ -64,9 +73,9 @@ public class Import : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinition
draft.MaterializerName = JsonWorkflowMaterializer.MaterializerName;
draft.Name = request.Name?.Trim();
draft.Description = request.Description?.Trim();
draft.Metadata = request.Metadata;
draft.Metadata = request.Metadata ?? new Dictionary<string, object>();
draft.Variables = variables;
draft.ApplicationProperties = request.ApplicationProperties;
draft.ApplicationProperties = request.ApplicationProperties ?? new Dictionary<string, object>();
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<WorkflowDefinitionRequest, WorkflowDefinition
draft.Description,
draft.CreatedAt,
draft.Version,
request.Variables,
request.Variables ?? new List<VariableDefinition>(),
draft.Metadata,
draft.ApplicationProperties,
draft.IsLatest,