Merge branch 'master' of https://github.com/elsa-workflows/elsa-core
This commit is contained in:
commit
d8c3504aa7
|
|
@ -47,6 +47,7 @@ export namespace Components {
|
|||
"activityBorderColor"?: (activity: ActivityModel) => string;
|
||||
"activityContextMenu"?: ActivityContextMenuState;
|
||||
"activityContextMenuButton"?: (activity: ActivityModel) => string;
|
||||
"connectionContextMenu"?: ActivityContextMenuState;
|
||||
"enableMultipleConnectionsFromSingleSource": boolean;
|
||||
"layoutDirection": LayoutDirection;
|
||||
"mode": WorkflowDesignerMode;
|
||||
|
|
@ -705,6 +706,7 @@ declare namespace LocalJSX {
|
|||
"activityBorderColor"?: (activity: ActivityModel) => string;
|
||||
"activityContextMenu"?: ActivityContextMenuState;
|
||||
"activityContextMenuButton"?: (activity: ActivityModel) => string;
|
||||
"connectionContextMenu"?: ActivityContextMenuState;
|
||||
"enableMultipleConnectionsFromSingleSource"?: boolean;
|
||||
"layoutDirection"?: LayoutDirection;
|
||||
"mode"?: WorkflowDesignerMode;
|
||||
|
|
@ -712,6 +714,7 @@ declare namespace LocalJSX {
|
|||
"onActivityContextMenuButtonClicked"?: (event: CustomEvent<ActivityContextMenuState>) => void;
|
||||
"onActivityDeselected"?: (event: CustomEvent<ActivityModel>) => void;
|
||||
"onActivitySelected"?: (event: CustomEvent<ActivityModel>) => void;
|
||||
"onConnectionContextMenuButtonClicked"?: (event: CustomEvent<ActivityContextMenuState>) => void;
|
||||
"onWorkflow-changed"?: (event: CustomEvent<WorkflowModel>) => void;
|
||||
"selectedActivityIds"?: Array<string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Component, Event, EventEmitter, h, Host, Method, Prop, State, Watch} from '@stencil/core';
|
||||
import {Component, Event, EventEmitter, h, Host, Method, Prop, State, Watch, Listen} from '@stencil/core';
|
||||
import {v4 as uuid} from 'uuid';
|
||||
import {addConnection, findActivity, getChildActivities, getInboundConnections, getOutboundConnections, Map, removeActivity, removeConnection} from '../../../../utils/utils';
|
||||
import {ActivityDescriptor, ActivityDesignDisplayContext, ActivityModel, ActivityTraits, ConnectionModel, EventTypes, WorkflowModel, WorkflowPersistenceBehavior,} from '../../../../models';
|
||||
|
|
@ -21,6 +21,7 @@ export class ElsaWorkflowDesigner {
|
|||
@Prop() activityContextMenuButton?: (activity: ActivityModel) => string;
|
||||
@Prop() activityBorderColor?: (activity: ActivityModel) => string;
|
||||
@Prop() activityContextMenu?: ActivityContextMenuState;
|
||||
@Prop() connectionContextMenu?: ActivityContextMenuState;
|
||||
@Prop() mode: WorkflowDesignerMode = WorkflowDesignerMode.Edit;
|
||||
@Prop() layoutDirection: LayoutDirection = LayoutDirection.Vertical;
|
||||
@Prop({attribute: 'enable-multiple-connections'}) enableMultipleConnectionsFromSingleSource: boolean;
|
||||
|
|
@ -28,6 +29,7 @@ export class ElsaWorkflowDesigner {
|
|||
@Event() activitySelected: EventEmitter<ActivityModel>;
|
||||
@Event() activityDeselected: EventEmitter<ActivityModel>;
|
||||
@Event() activityContextMenuButtonClicked: EventEmitter<ActivityContextMenuState>;
|
||||
@Event() connectionContextMenuButtonClicked: EventEmitter<ActivityContextMenuState>;
|
||||
@State() workflowModel: WorkflowModel;
|
||||
|
||||
@State() activityContextMenuState: ActivityContextMenuState = {
|
||||
|
|
@ -37,6 +39,13 @@ export class ElsaWorkflowDesigner {
|
|||
activity: null,
|
||||
};
|
||||
|
||||
@State() connectionContextMenuState: ActivityContextMenuState = {
|
||||
shown: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
activity: null,
|
||||
};
|
||||
|
||||
el: HTMLElement;
|
||||
svg: SVGSVGElement;
|
||||
inner: SVGGElement;
|
||||
|
|
@ -58,6 +67,11 @@ export class ElsaWorkflowDesigner {
|
|||
this.activityContextMenuButtonClicked.emit(state);
|
||||
}
|
||||
|
||||
handleConnectionContextMenuChange(state: ActivityContextMenuState) {
|
||||
this.connectionContextMenuState = state;
|
||||
this.connectionContextMenuButtonClicked.emit(state);
|
||||
}
|
||||
|
||||
@Watch('model')
|
||||
handleModelChanged(newValue: WorkflowModel) {
|
||||
this.updateWorkflowModel(newValue, false);
|
||||
|
|
@ -81,6 +95,11 @@ export class ElsaWorkflowDesigner {
|
|||
this.activityContextMenuState = newValue;
|
||||
}
|
||||
|
||||
@Watch('connectionContextMenu')
|
||||
handleConnectionContextMenuChanged(newValue: ActivityContextMenuState) {
|
||||
this.connectionContextMenuState = newValue;
|
||||
}
|
||||
|
||||
@Method()
|
||||
async removeActivity(activity: ActivityModel) {
|
||||
this.removeActivityInternal(activity);
|
||||
|
|
@ -89,16 +108,89 @@ export class ElsaWorkflowDesigner {
|
|||
@Method()
|
||||
async showActivityEditor(activity: ActivityModel, animate: boolean) {
|
||||
this.showActivityEditorInternal(activity, animate);
|
||||
}
|
||||
|
||||
@Listen('keydown', { target: 'document' })
|
||||
async handleKeyDown(event: KeyboardEvent){
|
||||
if((event.ctrlKey || event.metaKey) && event.key === 'c') {
|
||||
await this.copyActivitiesToClipboard();
|
||||
}
|
||||
if((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
||||
await this.pasteActivitiesFromClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
async copyActivitiesToClipboard()
|
||||
{
|
||||
this.checkClipboardPermissions();
|
||||
await navigator.clipboard.writeText(JSON.stringify(this.selectedActivities));
|
||||
eventBus.emit(EventTypes.ClipboardCopied, this);
|
||||
}
|
||||
|
||||
async pasteActivitiesFromClipboard()
|
||||
{
|
||||
this.checkClipboardPermissions();
|
||||
|
||||
let copiedActivities: Array<ActivityModel> = [];
|
||||
|
||||
await navigator.clipboard.readText().then(data => {
|
||||
copiedActivities = JSON.parse(data);
|
||||
});
|
||||
|
||||
this.addActivitiesFromClipboard(copiedActivities)
|
||||
}
|
||||
|
||||
async addActivitiesFromClipboard(copiedActivities: Array<ActivityModel>)
|
||||
{
|
||||
let sourceActivityId: string;
|
||||
this.parentActivityId = null;
|
||||
this.parentActivityOutcome = null;
|
||||
|
||||
for (const key in this.selectedActivities) {
|
||||
sourceActivityId = this.selectedActivities[key].activityId;
|
||||
}
|
||||
|
||||
if (sourceActivityId != undefined)
|
||||
{
|
||||
this.parentActivityId = sourceActivityId;
|
||||
this.parentActivityOutcome = this.selectedActivities[sourceActivityId].outcomes[0];
|
||||
}
|
||||
|
||||
for (const key in copiedActivities) {
|
||||
this.addingActivity = true;
|
||||
copiedActivities[key].activityId = uuid();
|
||||
|
||||
eventBus.emit(EventTypes.UpdateActivity, this, copiedActivities[key]);
|
||||
|
||||
this.parentActivityId = copiedActivities[key].activityId;
|
||||
this.parentActivityOutcome = copiedActivities[key].outcomes[0];
|
||||
}
|
||||
|
||||
this.selectedActivities = {};
|
||||
// Set to null to avoid conflict with on Activity node click event
|
||||
this.parentActivityId = null;
|
||||
this.parentActivityOutcome = null;
|
||||
}
|
||||
|
||||
checkClipboardPermissions()
|
||||
{
|
||||
navigator.permissions.query({ name: "clipboard-read" }).then((result) => {
|
||||
if (result.state == 'denied')
|
||||
eventBus.emit(EventTypes.ClipboardPermissionDenied, this);
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
eventBus.on(EventTypes.ActivityPicked, this.onActivityPicked);
|
||||
eventBus.on(EventTypes.UpdateActivity, this.onUpdateActivity);
|
||||
eventBus.on(EventTypes.PasteActivity, this.onPasteActivity);
|
||||
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
eventBus.detach(EventTypes.ActivityPicked, this.onActivityPicked);
|
||||
eventBus.detach(EventTypes.UpdateActivity, this.onUpdateActivity);
|
||||
eventBus.detach(EventTypes.PasteActivity, this.onPasteActivity);
|
||||
d3.selectAll('.node').on('click', null);
|
||||
d3.selectAll('.edgePath').on('contextmenu', null);
|
||||
}
|
||||
|
|
@ -485,6 +577,15 @@ export class ElsaWorkflowDesigner {
|
|||
}
|
||||
};
|
||||
|
||||
onPasteActivity = async args => {
|
||||
const activityModel = args as ActivityModel;
|
||||
|
||||
this.selectedActivities = {};
|
||||
activityModel.outcomes[0] = this.parentActivityOutcome;
|
||||
this.selectedActivities[activityModel.activityId] = activityModel;
|
||||
this.pasteActivitiesFromClipboard();
|
||||
};
|
||||
|
||||
tryRerenderTree(waitTime?: number, attempt?: number) {
|
||||
const maxTries = 3;
|
||||
|
||||
|
|
@ -539,6 +640,13 @@ export class ElsaWorkflowDesigner {
|
|||
})
|
||||
.on("mouseout", e => {
|
||||
d3.select(node.elem).select('svg').classed('elsa-text-green-400', false).classed('hover:elsa-text-blue-500', true);
|
||||
})
|
||||
.on('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.parentActivityId = node.activity.activityId;
|
||||
this.parentActivityOutcome = node.outcome;
|
||||
this.handleConnectionContextMenuChange({x: e.clientX, y: e.clientY, shown: true, activity: node.activity});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -576,21 +684,35 @@ export class ElsaWorkflowDesigner {
|
|||
const activity = node.activity;
|
||||
const activityId = activity.activityId;
|
||||
|
||||
d3.select(node.elem).on('click', () => {
|
||||
d3.select(node.elem).on('click', e => {
|
||||
// If a parent activity was selected to connect to:
|
||||
if (this.mode == WorkflowDesignerMode.Edit && this.parentActivityId && this.parentActivityOutcome) {
|
||||
this.addConnection(this.parentActivityId, activityId, this.parentActivityOutcome);
|
||||
} else {
|
||||
// When clicking an activity:
|
||||
if (!!this.selectedActivities[activityId])
|
||||
delete this.selectedActivities[activityId];
|
||||
else {
|
||||
for (const key in this.selectedActivities) {
|
||||
this.activityDeselected.emit(this.selectedActivities[key]);
|
||||
// When clicking an activity with shift:
|
||||
if (e.shiftKey)
|
||||
{
|
||||
if (!!this.selectedActivities[activityId])
|
||||
delete this.selectedActivities[activityId];
|
||||
else
|
||||
{
|
||||
this.selectedActivities[activityId] = activity;
|
||||
this.activitySelected.emit(activity);
|
||||
}
|
||||
}
|
||||
// When clicking an activity:
|
||||
else
|
||||
{
|
||||
if (!!this.selectedActivities[activityId])
|
||||
delete this.selectedActivities[activityId];
|
||||
else {
|
||||
for (const key in this.selectedActivities) {
|
||||
this.activityDeselected.emit(this.selectedActivities[key]);
|
||||
}
|
||||
this.selectedActivities = {};
|
||||
this.selectedActivities[activityId] = activity;
|
||||
this.activitySelected.emit(activity);
|
||||
}
|
||||
this.selectedActivities = {};
|
||||
this.selectedActivities[activityId] = activity;
|
||||
this.activitySelected.emit(activity);
|
||||
}
|
||||
|
||||
this.rerenderTree();
|
||||
|
|
|
|||
|
|
@ -14,15 +14,21 @@ export class ElsaWorkflowEditorNotifications {
|
|||
eventBus.on(EventTypes.WorkflowPublished, this.onWorkflowPublished);
|
||||
eventBus.on(EventTypes.WorkflowRetracted, this.onWorkflowRetracted);
|
||||
eventBus.on(EventTypes.WorkflowImported, this.onWorkflowImported);
|
||||
eventBus.on(EventTypes.ClipboardPermissionDenied, this.onClipboardPermissionsDenied);
|
||||
eventBus.on(EventTypes.ClipboardCopied, this.onClipboardCopied);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
eventBus.detach(EventTypes.WorkflowPublished, this.onWorkflowPublished);
|
||||
eventBus.detach(EventTypes.WorkflowRetracted, this.onWorkflowRetracted);
|
||||
eventBus.detach(EventTypes.WorkflowRetracted, this.onWorkflowRetracted);
|
||||
eventBus.detach(EventTypes.WorkflowImported, this.onWorkflowImported);
|
||||
eventBus.detach(EventTypes.ClipboardPermissionDenied, this.onClipboardPermissionsDenied);
|
||||
eventBus.detach(EventTypes.ClipboardCopied, this.onClipboardCopied);
|
||||
}
|
||||
|
||||
onWorkflowPublished = (workflowDefinition: WorkflowDefinition) => toastNotificationService.show('Workflow Published', `Workflow successfully published at version ${workflowDefinition.version}.`, 1500);
|
||||
onWorkflowRetracted = (workflowDefinition: WorkflowDefinition) => toastNotificationService.show('Workflow Retracted', `Workflow successfully retracted at version ${workflowDefinition.version}.`, 1500);
|
||||
onWorkflowImported = (workflowDefinition: WorkflowDefinition) => toastNotificationService.show('Workflow Imported', `Workflow successfully imported.`, 1500);
|
||||
onWorkflowImported = () => toastNotificationService.show('Workflow Imported', `Workflow successfully imported.`, 1500);
|
||||
onClipboardPermissionsDenied = () => toastNotificationService.show('Clipboard Error', `Clipboard pemission denied.`, 1500);
|
||||
onClipboardCopied = () => toastNotificationService.show('Copy to Clipboard', `Activities successfully copied to Clipboard.`, 1500);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,13 @@ export class ElsaWorkflowDefinitionEditorScreen {
|
|||
activity: null,
|
||||
};
|
||||
|
||||
@State() connectionContextMenuState: ActivityContextMenuState = {
|
||||
shown: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
activity: null,
|
||||
};
|
||||
|
||||
i18next: i18n;
|
||||
el: HTMLElement;
|
||||
designer: HTMLElsaDesignerTreeElement;
|
||||
|
|
@ -241,6 +248,7 @@ export class ElsaWorkflowDefinitionEditorScreen {
|
|||
|
||||
try {
|
||||
console.debug("Saving workflow...");
|
||||
|
||||
workflowDefinition = await client.workflowDefinitionsApi.save(request);
|
||||
this.workflowDefinition = workflowDefinition;
|
||||
this.workflowModel = this.mapWorkflowModel(workflowDefinition);
|
||||
|
|
@ -319,6 +327,10 @@ export class ElsaWorkflowDefinitionEditorScreen {
|
|||
this.activityContextMenuState = state;
|
||||
}
|
||||
|
||||
handleConnectionContextMenuChange(state: ActivityContextMenuState) {
|
||||
this.connectionContextMenuState = state;
|
||||
}
|
||||
|
||||
onShowWorkflowSettingsClick() {
|
||||
eventBus.emit(EventTypes.ShowWorkflowSettings);
|
||||
}
|
||||
|
|
@ -351,10 +363,21 @@ export class ElsaWorkflowDefinitionEditorScreen {
|
|||
this.handleContextMenuChange({x: 0, y: 0, shown: false, activity: null});
|
||||
}
|
||||
|
||||
async onPasteActivityClick(e: Event) {
|
||||
e.preventDefault();
|
||||
let activityModel = this.connectionContextMenuState.activity;
|
||||
eventBus.emit(EventTypes.PasteActivity, this, activityModel);
|
||||
this.handleConnectionContextMenuChange({x: 0, y: 0, shown: false, activity: null});
|
||||
}
|
||||
|
||||
onActivityContextMenuButtonClicked(e: CustomEvent<ActivityContextMenuState>) {
|
||||
this.activityContextMenuState = e.detail;
|
||||
}
|
||||
|
||||
onConnectionContextMenuButtonClicked(e: CustomEvent<ActivityContextMenuState>) {
|
||||
this.connectionContextMenuState = e.detail;
|
||||
}
|
||||
|
||||
private onUpdateWorkflowSettings = async (workflowDefinition: WorkflowDefinition) => {
|
||||
this.updateWorkflowDefinition(workflowDefinition);
|
||||
await this.saveWorkflowInternal(this.workflowModel);
|
||||
|
|
@ -399,6 +422,7 @@ export class ElsaWorkflowDefinitionEditorScreen {
|
|||
mode={WorkflowDesignerMode.Edit}
|
||||
activityContextMenuButton={activityContextMenuButton}
|
||||
onActivityContextMenuButtonClicked={e => this.onActivityContextMenuButtonClicked(e)}
|
||||
onConnectionContextMenuButtonClicked={e => this.onConnectionContextMenuButtonClicked(e)}
|
||||
activityContextMenu={this.activityContextMenuState}
|
||||
enableMultipleConnectionsFromSingleSource={false}
|
||||
class="elsa-flex-1"
|
||||
|
|
@ -406,6 +430,7 @@ export class ElsaWorkflowDefinitionEditorScreen {
|
|||
{this.renderPropertiesPanel()}
|
||||
{this.renderWorkflowSettingsButton()}
|
||||
{this.renderActivityContextMenu()}
|
||||
{this.renderConnectionContextMenu()}
|
||||
<elsa-workflow-settings-modal workflowDefinition={this.workflowDefinition}/>
|
||||
<elsa-workflow-definition-editor-notifications/>
|
||||
<div class="elsa-fixed elsa-bottom-10 elsa-right-12">
|
||||
|
|
@ -461,6 +486,38 @@ export class ElsaWorkflowDefinitionEditorScreen {
|
|||
</div>
|
||||
}
|
||||
|
||||
renderConnectionContextMenu() {
|
||||
const t = this.t;
|
||||
|
||||
return <div
|
||||
data-transition-enter="elsa-transition elsa-ease-out elsa-duration-100"
|
||||
data-transition-enter-start="elsa-transform elsa-opacity-0 elsa-scale-95"
|
||||
data-transition-enter-end="elsa-transform elsa-opacity-100 elsa-scale-100"
|
||||
data-transition-leave="elsa-transition elsa-ease-in elsa-duration-75"
|
||||
data-transition-leave-start="elsa-transform elsa-opacity-100 elsa-scale-100"
|
||||
data-transition-leave-end="elsa-transform elsa-opacity-0 elsa-scale-95"
|
||||
class={`${this.connectionContextMenuState.shown ? '' : 'hidden'} context-menu elsa-z-10 elsa-mx-3 elsa-w-48 elsa-mt-1 elsa-rounded-md elsa-shadow-lg elsa-absolute`}
|
||||
style={{left: `${this.connectionContextMenuState.x}px`, top: `${this.connectionContextMenuState.y - 64}px`}}
|
||||
ref={el =>
|
||||
registerClickOutside(this, el, () => {
|
||||
this.handleConnectionContextMenuChange({x: 0, y: 0, shown: false, activity: null});
|
||||
})
|
||||
}
|
||||
>
|
||||
<div class="elsa-rounded-md elsa-bg-white elsa-shadow-xs" role="menu" aria-orientation="vertical" aria-labelledby="pinned-project-options-menu-0">
|
||||
<div class="elsa-py-1">
|
||||
<a
|
||||
onClick={e => this.onPasteActivityClick(e)}
|
||||
href="#"
|
||||
class="elsa-block elsa-px-4 elsa-py-2 elsa-text-sm elsa-leading-5 elsa-text-gray-700 hover:elsa-bg-gray-100 hover:elsa-text-gray-900 focus:elsa-outline-none focus:elsa-bg-gray-100 focus:elsa-text-gray-900"
|
||||
role="menuitem">
|
||||
{t('ConnectionContextMenu.Paste')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
renderActivityPicker() {
|
||||
return <elsa-activity-picker-modal/>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"ActivityContextMenu":{
|
||||
"Edit": "Edit",
|
||||
"Delete": "Delete"
|
||||
},
|
||||
"ConnectionContextMenu":{
|
||||
"Paste": "Paste",
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -16,6 +19,9 @@
|
|||
"ActivityContextMenu":{
|
||||
"Edit": "Bewerken",
|
||||
"Delete": "Verwijderen"
|
||||
},
|
||||
"ConnectionContextMenu":{
|
||||
"Paste": "Plakken",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ export const EventTypes = {
|
|||
ShowConfirmDialog: 'show-confirm-dialog',
|
||||
HideConfirmDialog: 'hide-confirm-dialog',
|
||||
ShowToastNotification: 'show-toast-notification',
|
||||
HideToastNotification: 'hide-toast-notification'
|
||||
HideToastNotification: 'hide-toast-notification',
|
||||
ClipboardPermissionDenied: 'clipboard-permission-denied',
|
||||
ClipboardCopied: 'clipboard-copied',
|
||||
PasteActivity: 'paste-activity',
|
||||
};
|
||||
|
||||
export interface AddActivityEventArgs {
|
||||
|
|
|
|||
Loading…
Reference in a new issue