Update import / export (#3471)
This commit is contained in:
parent
ae1590220d
commit
6bab9982cd
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue