From 016e642ac4dc5c02e50fede311d6484176ece85f Mon Sep 17 00:00:00 2001 From: gurkanguran <54795879+gurkanguran@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:34:32 +0100 Subject: [PATCH] Auto rotating (#3482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implemented auto-orientation of ports and edges * Added mapping for different connection models on server/client sides * Increased the size of circles Fixes #2893 Co-authored-by: Gürkan Güran --- .../Properties/launchSettings.json | 2 +- .../src/components.d.ts | 8 +- .../src/components/designer/canvas/canvas.tsx | 4 +- .../src/components/inputs/checkbox.tsx | 1 - .../modules/flowchart/default-node-handler.ts | 27 ++- .../src/modules/flowchart/flowchart.tsx | 19 +- .../src/modules/flowchart/graph-factory.ts | 36 +++- .../src/modules/flowchart/ports.tsx | 31 --- .../src/modules/flowchart/shapes.ts | 54 ++++- .../components/editor.tsx | 5 +- .../components/toolbar.tsx | 9 +- .../workflow-definitions/services/api.ts | 13 +- .../workflow-definitions/services/manager.ts | 1 + .../services/container-activity-component.tsx | 2 +- .../src/utils/graph.ts | 196 ++++++++++++++++++ 15 files changed, 325 insertions(+), 83 deletions(-) create mode 100644 src/designer/elsa-workflows-designer/src/utils/graph.ts diff --git a/src/bundles/Elsa.WorkflowServer.Web/Properties/launchSettings.json b/src/bundles/Elsa.WorkflowServer.Web/Properties/launchSettings.json index ab15a90c8..65a64df73 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Properties/launchSettings.json +++ b/src/bundles/Elsa.WorkflowServer.Web/Properties/launchSettings.json @@ -12,7 +12,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "https://localhost:7072;http://localhost:5072", + "applicationUrl": "https://localhost:5001;http://localhost:5072", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index 772ccf351..150dc8675 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -68,7 +68,7 @@ export namespace Components { } interface ElsaCanvas { "addActivity": (args: AddActivityArgs) => Promise; - "autoLayout": () => Promise; + "autoLayout": (direction: "TB" | "BT" | "LR" | "RL") => Promise; "exportGraph": () => Promise; "getRootComponent": () => Promise; "importGraph": (root: Activity) => Promise; @@ -121,7 +121,7 @@ export namespace Components { } interface ElsaFlowchart { "addActivity": (args: AddActivityArgs) => Promise; - "autoLayout": () => Promise; + "autoLayout": (direction: "TB" | "BT" | "LR" | "RL") => Promise; "export": () => Promise; "getCurrentLevel": () => Promise; "getGraph": () => Promise; @@ -267,7 +267,7 @@ export namespace Components { "workflowDefinition"?: WorkflowDefinition; } interface ElsaWorkflowDefinitionEditorToolbar { - "autoLayout": () => Promise; + "autoLayout": (direction: "TB" | "BT" | "LR" | "RL") => Promise; "zoomToFit": () => Promise; } interface ElsaWorkflowDefinitionEditorToolbox { @@ -955,7 +955,7 @@ declare namespace LocalJSX { "workflowDefinition"?: WorkflowDefinition; } interface ElsaWorkflowDefinitionEditorToolbar { - "autoLayout"?: () => Promise; + "autoLayout"?: (direction: "TB" | "BT" | "LR" | "RL") => Promise; "zoomToFit"?: () => Promise; } interface ElsaWorkflowDefinitionEditorToolbox { diff --git a/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.tsx b/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.tsx index 1179f0887..e16a17abd 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/canvas/canvas.tsx @@ -67,8 +67,8 @@ export class Canvas { } @Method() - public async autoLayout(): Promise { - return await this.root.autoLayout(); + public async autoLayout(direction: "TB" | "BT" | "LR" | "RL"): Promise { + return await this.root.autoLayout(direction); } @Method() diff --git a/src/designer/elsa-workflows-designer/src/components/inputs/checkbox.tsx b/src/designer/elsa-workflows-designer/src/components/inputs/checkbox.tsx index 0398d0232..1f28f064d 100644 --- a/src/designer/elsa-workflows-designer/src/components/inputs/checkbox.tsx +++ b/src/designer/elsa-workflows-designer/src/components/inputs/checkbox.tsx @@ -50,7 +50,6 @@ export class Checkbox { } private onExpressionChanged = (e: CustomEvent) => { - debugger; this.inputContext.inputChanged(e.detail.expression, e.detail.syntax); } } diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/default-node-handler.ts b/src/designer/elsa-workflows-designer/src/modules/flowchart/default-node-handler.ts index c17326130..487702ec2 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/default-node-handler.ts +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/default-node-handler.ts @@ -4,6 +4,7 @@ import {Container, Service} from "typedi" import {ActivityNodeHandler, CreateUINodeContext} from "./activity-node-handler"; import {PortProviderContext, PortProviderRegistry} from "../../services"; import {PortMode} from "../../models"; +import {v4 as uuid} from 'uuid'; @Service() export class DefaultNodeHandler implements ActivityNodeHandler { @@ -27,27 +28,31 @@ export class DefaultNodeHandler implements ActivityNodeHandler { if (outPorts.length == 1) outPorts[0].displayName = null; - const inPortModels = inPorts.map(x => ({ - id: x.name, - group: 'in', + const leftPortModels = inPorts.map((x) => ({ + id: uuid() + '_' + x.name, + group: 'left', attrs: !!x.displayName ? { text: { text: x.displayName - } - } : null + }, + } : null, + type:'in', + position:'left' })); - const outPortModels = outPorts.map(x => ({ - id: x.name, - group: 'out', + const rightPortModels = outPorts.map((x) => ({ + id: uuid() + '_' + x.name, + group: 'right', attrs: { text: { text: x.displayName - } - } + }, + }, + type: 'out', + position: 'right' })); - const portModels = [...inPortModels, ...outPortModels]; + const portModels = [...leftPortModels, ...rightPortModels]; return { id: activity.id, diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx b/src/designer/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx index 3445b714b..c61bad211 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/flowchart.tsx @@ -20,7 +20,8 @@ import PositionEventArgs = NodeView.PositionEventArgs; import FromJSONData = Model.FromJSONData; import PointLike = Point.PointLike; import {generateUniqueActivityName} from "../../utils/generate-activity-name"; -import {DagreLayout, OutNode} from '@antv/layout'; +import { DagreLayout, OutNode} from '@antv/layout'; +import { adjustPortMarkupByNode, rebuildGraph } from '../../utils/graph'; import {WorkflowDefinition} from "../workflow-definitions/models/entities"; import FlowchartTunnel, {FlowchartState} from "./state"; import WorkflowDefinitionTunnel, {WorkflowDefinitionState} from "../../state/workflow-definition-state"; @@ -125,11 +126,10 @@ export class FlowchartComponent implements ContainerActivityComponent { } @Method() - async autoLayout() { - + async autoLayout(direction: "TB" | "BT" | "LR" | "RL") { const dagreLayout = new DagreLayout({ type: 'dagre', - rankdir: 'TB', + rankdir: direction, align: 'UL', ranksep: 30, nodesep: 15, @@ -161,8 +161,7 @@ export class FlowchartComponent implements ContainerActivityComponent { this.updateActivity({id: activity.id, originalId: activity.id, activity: activity}); }); - await this.import(this.activity); - await this.scrollToStart(); + this.import(this.activity); } @Method() @@ -191,6 +190,8 @@ export class FlowchartComponent implements ContainerActivityComponent { const node = this.nodeFactory.createNode(descriptor, activity, sx, sy); graph.addNode(node, {merge: true}); + + adjustPortMarkupByNode(graph.getNodes().find(n => n.id == node.id)); await this.updateModel(); return activity; } @@ -257,7 +258,11 @@ export class FlowchartComponent implements ContainerActivityComponent { @Method() async import(root: Activity): Promise { - return await this.importInternal(root); + await this.importInternal(root); + + rebuildGraph(this.graph); + + await this.scrollToStart(); } @Method() diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/graph-factory.ts b/src/designer/elsa-workflows-designer/src/modules/flowchart/graph-factory.ts index 9987daa0b..622f5fc0f 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/graph-factory.ts +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/graph-factory.ts @@ -1,11 +1,10 @@ import {CellView, Graph, Node, Shape} from '@antv/x6'; +import { autoOrientPortsAndEdges } from '../../utils/graph'; import './ports'; import {Activity} from "../../models"; import {Connection} from "./models"; import descriptorsStore from "../../data/descriptors-store"; import {generateUniqueActivityName} from "../../utils/generate-activity-name"; -import {Hash} from "../../utils"; -import {createActivityLookup} from "../../services"; export function createGraph( container: HTMLElement, @@ -69,8 +68,8 @@ export function createGraph( router: { name: 'manhattan', args: { - startDirections: ['right'], - endDirections: ['left'], + startDirections: ['top','right','left','bottom'], + endDirections: ['top','right','left','bottom'], }, }, // router: { @@ -90,23 +89,34 @@ export function createGraph( snap: { radius: 20, }, - validateMagnet({magnet}) { - return magnet.getAttribute('port-group') !== 'in' + validateMagnet({view, magnet}) { + const node = view.cell as Node; + const sourcePort = node.getPort(magnet.getAttribute('port')); + return sourcePort.type !== 'in' }, validateConnection({sourceView, targetView, sourceMagnet, targetMagnet}) { - if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') { + if(!sourceMagnet || !targetMagnet) { + return false; + } + + const sourceNode = sourceView.cell as Node; + const sourcePort = sourceNode.getPort(sourceMagnet.getAttribute('port')); + + const targetNode = targetView.cell as Node; + const targetPort = targetNode.getPort(targetMagnet.getAttribute('port')); + + if (sourcePort.type === 'in') { return false } - if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') { + if (targetPort.type !== 'in') { return false } const portId = targetMagnet.getAttribute('port')! const node = targetView.cell as Node const port = node.getPort(portId) - return !(port && port.connected); - + return !(targetPort && targetPort.connected); }, createEdge() { return graph.createEdge({ @@ -276,5 +286,9 @@ export function createGraph( } }); + graph.on("node:moving", ({ node }) => { + autoOrientPortsAndEdges(graph, node); + }); + return graph; -}; +}; \ No newline at end of file diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/ports.tsx b/src/designer/elsa-workflows-designer/src/modules/flowchart/ports.tsx index 408599f33..59af060d7 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/ports.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/ports.tsx @@ -1,35 +1,4 @@ import {Graph, Line, Path} from "@antv/x6"; -import {toResult} from "@antv/x6/lib/registry/port-layout/util"; - -Graph.registerPortLayout('dynamicOut', (portsPositionArgs, elemBBox) => { - return portsPositionArgs.map((_, index) => { - - const portCount = portsPositionArgs.length; - const ratio = (index + 0.5) / portCount; - const p1 = portCount <= 3 ? elemBBox.getTopRight() : elemBBox.getBottomLeft(); - const p2 = portCount <= 3 ? elemBBox.getBottomRight() : elemBBox.getBottomRight(); - const line = new Line(p1, p2) - const p = line.pointAt(ratio); - - return toResult(p.round(), 0, {}); - - }); -}); - -Graph.registerPortLayout('dynamicIn', (portsPositionArgs, elemBBox) => { - return portsPositionArgs.map((_, index) => { - - const portCount = portsPositionArgs.length; - const ratio = (index + 0.5) / portCount; - const p1 = portCount <= 3 ? elemBBox.getTopLeft() : elemBBox.getTopLeft(); - const p2 = portCount <= 3 ? elemBBox.getBottomLeft() : elemBBox.getTopRight(); - const line = new Line(p1, p2) - const p = line.pointAt(ratio); - - return toResult(p.round(), 0, {}); - - }); -}); Graph.registerConnector( 'elsa-connector', diff --git a/src/designer/elsa-workflows-designer/src/modules/flowchart/shapes.ts b/src/designer/elsa-workflows-designer/src/modules/flowchart/shapes.ts index 72613e7a2..dc0bfcdcd 100644 --- a/src/designer/elsa-workflows-designer/src/modules/flowchart/shapes.ts +++ b/src/designer/elsa-workflows-designer/src/modules/flowchart/shapes.ts @@ -114,8 +114,8 @@ export class ActivityNodeShape extends Shape.HTML { ActivityNodeShape.config({ ports: { groups: { - in: { - position: 'dynamicIn', + left: { + position: 'left', attrs: { circle: { r: 6, @@ -135,15 +135,57 @@ ActivityNodeShape.config({ }, }, }, - out: { - position: 'dynamicOut', + right: { + position: 'right', attrs: { circle: { r: 6, magnet: true, - stroke: '#fff', + stroke: '#3c82f6', strokeWidth: 2, - fill: '#3c82f6', + fill: '#fff', + }, + text: { + fontSize: 12, + fill: '#888', + }, + }, + label: { + position: { + name: 'outside', + }, + }, + }, + top: { + position: 'top', + attrs: { + circle: { + r: 6, + magnet: true, + stroke: '#3c82f6', + strokeWidth: 2, + fill: '#fff', + }, + text: { + fontSize: 12, + fill: '#888', + }, + }, + label: { + position: { + name: 'outside', + }, + }, + }, + bottom: { + position: 'bottom', + attrs: { + circle: { + r: 6, + magnet: true, + stroke: '#3c82f6', + strokeWidth: 2, + fill: '#fff', }, text: { fontSize: 12, diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/editor.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/editor.tsx index 557d83185..f2ae7a51d 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/editor.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/editor.tsx @@ -114,7 +114,6 @@ export class WorkflowDefinitionEditor { await this.updateWorkflowDefinition(workflowDefinition); await this.canvas.importGraph(workflowDefinition.root); await this.eventBus.emit(WorkflowEditorEventTypes.WorkflowDefinition.Imported, this, {workflowDefinition}); - await this.canvas.scrollToStart(); } // Updates the workflow definition without importing it into the designer. @@ -229,9 +228,9 @@ export class WorkflowDefinitionEditor { }); }; - private onZoomToFit = async () => await this.canvas.zoomToFit() + private onZoomToFit = async () => await this.canvas.zoomToFit(); - private onAutoLayout = async () => await this.canvas.autoLayout() + private onAutoLayout = async (direction: "TB" | "BT" | "LR" | "RL") => await this.canvas.autoLayout(direction); private onActivityUpdated = async (e: CustomEvent) => { await this.canvas.updateActivity({ diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/toolbar.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/toolbar.tsx index c94128da4..50a2346ba 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/toolbar.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-definitions/components/toolbar.tsx @@ -8,13 +8,16 @@ export class Toolbar { public zoomToFit: () => Promise; @Prop() - public autoLayout: () => Promise; + public autoLayout: (direction: "TB" | "BT" | "LR" | "RL") => Promise; render() { return (
- +