diff --git a/src/designer/elsa-workflows-studio/package-lock.json b/src/designer/elsa-workflows-studio/package-lock.json index b3b7aedd4..1ceec272c 100644 --- a/src/designer/elsa-workflows-studio/package-lock.json +++ b/src/designer/elsa-workflows-studio/package-lock.json @@ -19,6 +19,7 @@ "autoprefixer": "10.2.4", "axios": "^0.21.1", "cssnano": "^4.1.10", + "dragula": "^3.7.3", "el-transition": "^0.0.7", "eslint": "7.20.0", "js-event-bus": "^1.0.3", @@ -287,6 +288,11 @@ "node": ">= 4.0.0" } }, + "node_modules/atoa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atoa/-/atoa-1.0.0.tgz", + "integrity": "sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk=" + }, "node_modules/autoprefixer": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.4.tgz", @@ -527,6 +533,15 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "node_modules/contra": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/contra/-/contra-1.9.4.tgz", + "integrity": "sha1-9TveQtfltZhcrk2ZqNYQUm3o8o0=", + "dependencies": { + "atoa": "1.0.0", + "ticky": "1.0.1" + } + }, "node_modules/copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -591,6 +606,14 @@ "node": ">= 8" } }, + "node_modules/crossvent": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.5.tgz", + "integrity": "sha1-rSCHjkkh6b5z2daXb4suzQ9xoLE=", + "dependencies": { + "custom-event": "^1.0.0" + } + }, "node_modules/css-blank-pseudo": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", @@ -943,6 +966,11 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=" + }, "node_modules/debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -1054,6 +1082,15 @@ "node": ">=8" } }, + "node_modules/dragula": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.3.tgz", + "integrity": "sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==", + "dependencies": { + "contra": "1.9.4", + "crossvent": "1.5.5" + } + }, "node_modules/el-transition": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/el-transition/-/el-transition-0.0.7.tgz", @@ -4612,6 +4649,11 @@ "xtend": "~4.0.1" } }, + "node_modules/ticky": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz", + "integrity": "sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0=" + }, "node_modules/timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", @@ -5185,6 +5227,11 @@ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" }, + "atoa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atoa/-/atoa-1.0.0.tgz", + "integrity": "sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk=" + }, "autoprefixer": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.4.tgz", @@ -5385,6 +5432,15 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "contra": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/contra/-/contra-1.9.4.tgz", + "integrity": "sha1-9TveQtfltZhcrk2ZqNYQUm3o8o0=", + "requires": { + "atoa": "1.0.0", + "ticky": "1.0.1" + } + }, "copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -5435,6 +5491,14 @@ "which": "^2.0.1" } }, + "crossvent": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.5.tgz", + "integrity": "sha1-rSCHjkkh6b5z2daXb4suzQ9xoLE=", + "requires": { + "custom-event": "^1.0.0" + } + }, "css-blank-pseudo": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", @@ -5713,6 +5777,11 @@ } } }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=" + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -5805,6 +5874,15 @@ "is-obj": "^2.0.0" } }, + "dragula": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.3.tgz", + "integrity": "sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==", + "requires": { + "contra": "1.9.4", + "crossvent": "1.5.5" + } + }, "el-transition": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/el-transition/-/el-transition-0.0.7.tgz", @@ -8770,6 +8848,11 @@ "xtend": "~4.0.1" } }, + "ticky": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz", + "integrity": "sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0=" + }, "timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx index d52be5676..dda4aefc6 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx @@ -204,119 +204,6 @@ export class ElsaWorkflowDesigner { this.workflowChanged.emit(model); } - onConnectionCreated(connection: ConnectionModel) { - const workflowModel = {...this.workflowModel}; - workflowModel.connections = [...workflowModel.connections, connection]; - - this.updateWorkflowModel(workflowModel); - } - - onConnectionDetached(connection: ConnectionModel) { - const workflowModel = {...this.workflowModel}; - workflowModel.connections = workflowModel.connections.filter(x => !(x.sourceId == connection.sourceId && x.targetId == connection.targetId && x.outcome == connection.outcome)); - - this.updateWorkflowModel(workflowModel); - } - - onAddButtonClick() { - this.showActivityPicker(); - } - - onOutcomeButtonClick(e: Event, outcome?: string, activityId?: string) { - e.preventDefault(); - this.parentActivityId = activityId; - this.parentActivityOutcome = outcome; - this.showActivityPicker(); - } - - render() { - const renderedActivities = new Set(); - - return ( - this.el = el}> -
-
-
this.canvasElement = el}> -
-
    -
  • -
    - -
    - {this.renderTree(this.getRootActivities(), true, renderedActivities)} -
  • -
-
-
-
-
-
- ); - } - - renderTree(activities: Array, isRoot: boolean, renderedActivities: Set): any { - const list = activities.filter(x => !renderedActivities.has(x.activityId)); - const cssClass = isRoot ? "root" : undefined; - - for (const activity of list) - renderedActivities.add(activity.activityId); - - if (list.length == 0) - return null; - - return ( -
    - {activities.map(x => { - const activityId = x.activityId; - const children = getChildActivities(this.workflowModel, activityId); - const displayContext = this.activityDisplayContexts[activityId]; - - return
  • -
    - {isRoot ? this.renderOutcomeButton(`start-button-plus-${x.activityId}`, '', e => this.onOutcomeButtonClick(e, null, null)) : undefined} - {this.renderActivity(displayContext)} - {this.renderOutcomeButtons(displayContext)} -
    - {children.length > 0 ? this.renderTree(children, false, renderedActivities) : undefined} -
  • ; - })} -
- ); - } - - renderOutcomeButtons(displayContext: ActivityDesignDisplayContext) { - const activityId = displayContext.activityModel.activityId; - - return ( -
- {displayContext.outcomes.map(x => this.renderOutcomeButton(`${activityId}-${x}`, x, e => this.onOutcomeButtonClick(e, x, activityId)))} -
- ); - } - - renderOutcomeButton(id: string, outcome: string, clickHandler: (e: MouseEvent) => void) { - return ( -
- {outcome && outcome.length > 0 ?
{outcome}
: undefined} - clickHandler(e)}> - - - - -
- ); - } - - renderActivity(displayContext: ActivityDesignDisplayContext) { - return ; - } - getJsPlumbConnections(): Array { const rootActivities = getChildActivities(this.workflowModel, null); @@ -395,4 +282,181 @@ export class ElsaWorkflowDesigner { } )); } + + onConnectionCreated(connection: ConnectionModel) { + const workflowModel = {...this.workflowModel}; + workflowModel.connections = [...workflowModel.connections, connection]; + + this.updateWorkflowModel(workflowModel); + } + + onConnectionDetached(connection: ConnectionModel) { + const workflowModel = {...this.workflowModel}; + workflowModel.connections = workflowModel.connections.filter(x => !(x.sourceId == connection.sourceId && x.targetId == connection.targetId && x.outcome == connection.outcome)); + + this.updateWorkflowModel(workflowModel); + } + + onAddButtonClick() { + this.showActivityPicker(); + } + + onOutcomeButtonClick(e: Event, outcome?: string, activityId?: string) { + e.preventDefault(); + this.parentActivityId = activityId; + this.parentActivityOutcome = outcome; + this.showActivityPicker(); + } + + async onDragStartActivity(e: DragEvent, activityDisplayContext: ActivityDesignDisplayContext) { + const activityModel = activityDisplayContext.activityModel; + e.dataTransfer.setData("text", activityModel.activityId); + e.dataTransfer.effectAllowed = 'move'; + } + + onDragOverOutcome(e: DragEvent, outcome: string, sourceActivityId: string) { + e.preventDefault(); + } + + onDropOnOutcome(e: DragEvent, outcome: string, sourceActivityId) { + const targetActivityId = e.dataTransfer.getData('text'); + + if (sourceActivityId === targetActivityId) + return; + + e.preventDefault(); + + const workflowModel = {...this.workflowModel}; + const targetActivityDisplayContext = this.activityDisplayContexts[targetActivityId]; + let connections = [...workflowModel.connections]; + + // Remove current connection where dropped activity is target. + connections = connections.filter(x => x.targetId !== targetActivityId) + + // Check if there is already a connection. + const existingConnection = connections.find(x => x.sourceId == sourceActivityId && x.outcome == outcome); + + if (existingConnection) { + // Try to find a matching outcome on the dropped activity to attach the existing activity to. + const newOutcome = targetActivityDisplayContext.outcomes.find(x => x == existingConnection.outcome) || targetActivityDisplayContext.outcomes[0]; + + existingConnection.sourceId = targetActivityId; + existingConnection.outcome = newOutcome; + + // Create new connection. + const newConnection: ConnectionModel = { + sourceId: sourceActivityId, + targetId: targetActivityId, + outcome: outcome + }; + + workflowModel.connections = [...connections, newConnection]; + } + else + { + // Create new connection. + const newConnection: ConnectionModel = { + sourceId: sourceActivityId, + targetId: targetActivityId, + outcome: outcome + }; + + workflowModel.connections = [...connections, newConnection]; + } + + this.updateWorkflowModel(workflowModel); + } + + render() { + const renderedActivities = new Set(); + + return ( + this.el = el}> +
+
+
this.canvasElement = el}> +
+
    +
  • +
    + +
    + {this.renderTree(this.getRootActivities(), true, renderedActivities)} +
  • +
+
+
+
+
+
+ ); + } + + renderTree(activities: Array, isRoot: boolean, renderedActivities: Set): any { + const list = activities.filter(x => !renderedActivities.has(x.activityId)); + const cssClass = isRoot ? "root" : undefined; + + for (const activity of list) + renderedActivities.add(activity.activityId); + + if (list.length == 0) + return null; + + return ( +
    + {activities.map(x => { + const activityId = x.activityId; + const children = getChildActivities(this.workflowModel, activityId); + const displayContext = this.activityDisplayContexts[activityId]; + + return
  • +
    + {isRoot ? this.renderOutcomeButton(`start-button-plus-${x.activityId}`, '', activityId, e => this.onOutcomeButtonClick(e, null, null)) : undefined} + {this.renderActivity(displayContext)} + {this.renderOutcomeButtons(displayContext)} +
    + {children.length > 0 ? this.renderTree(children, false, renderedActivities) : undefined} +
  • ; + })} +
+ ); + } + + renderOutcomeButtons(displayContext: ActivityDesignDisplayContext) { + const activityId = displayContext.activityModel.activityId; + + return ( +
+ {displayContext.outcomes.map(x => this.renderOutcomeButton(`${activityId}-${x}`, x, activityId, e => this.onOutcomeButtonClick(e, x, activityId)))} +
+ ); + } + + renderOutcomeButton(id: string, outcome: string, activityId: string, clickHandler: (e: MouseEvent) => void) { + return ( + + ); + } + + renderActivity(displayContext: ActivityDesignDisplayContext) { + return this.onDragStartActivity(e, displayContext)} + />; + } } diff --git a/src/server/Elsa.Server.Api/Endpoints/WorkflowDefinitions/Save.cs b/src/server/Elsa.Server.Api/Endpoints/WorkflowDefinitions/Save.cs index 37550649b..2a23c5112 100644 --- a/src/server/Elsa.Server.Api/Endpoints/WorkflowDefinitions/Save.cs +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowDefinitions/Save.cs @@ -73,7 +73,7 @@ namespace Elsa.Server.Api.Endpoints.WorkflowDefinitions from connection in request.Connections let sourceActivity = request.Activities.FirstOrDefault(x => x.ActivityId == connection.SourceActivityId) let targetActivity = request.Activities.FirstOrDefault(x => x.ActivityId == connection.TargetActivityId) - where sourceActivity != null && targetActivity != null + where sourceActivity != null && targetActivity != null && connection.Outcome is not null and not "" select connection; return validConnections;