Fix merge of auto rotating ports

This commit is contained in:
Sipke Schoorstra 2022-12-05 12:21:07 +01:00
parent 6ada301b1f
commit c9f1021a4b
12 changed files with 114 additions and 93 deletions

View file

@ -68,7 +68,7 @@ export namespace Components {
}
interface ElsaCanvas {
"addActivity": (args: AddActivityArgs) => Promise<Activity>;
"autoLayout": () => Promise<void>;
"autoLayout": (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
"exportGraph": () => Promise<Activity>;
"getRootComponent": () => Promise<ContainerActivityComponent>;
"importGraph": (root: Activity) => Promise<void>;
@ -121,7 +121,7 @@ export namespace Components {
}
interface ElsaFlowchart {
"addActivity": (args: AddActivityArgs) => Promise<Activity>;
"autoLayout": () => Promise<void>;
"autoLayout": (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
"export": () => Promise<Activity>;
"getCurrentLevel": () => Promise<Activity>;
"getGraph": () => Promise<Graph>;
@ -267,7 +267,7 @@ export namespace Components {
"workflowDefinition"?: WorkflowDefinition;
}
interface ElsaWorkflowDefinitionEditorToolbar {
"autoLayout": () => Promise<void>;
"autoLayout": (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
"zoomToFit": () => Promise<void>;
}
interface ElsaWorkflowDefinitionEditorToolbox {
@ -955,7 +955,7 @@ declare namespace LocalJSX {
"workflowDefinition"?: WorkflowDefinition;
}
interface ElsaWorkflowDefinitionEditorToolbar {
"autoLayout"?: () => Promise<void>;
"autoLayout"?: (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
"zoomToFit"?: () => Promise<void>;
}
interface ElsaWorkflowDefinitionEditorToolbox {

View file

@ -67,8 +67,8 @@ export class Canvas {
}
@Method()
public async autoLayout(): Promise<void> {
return await this.root.autoLayout();
public async autoLayout(direction: "TB" | "BT" | "LR" | "RL"): Promise<void> {
return await this.root.autoLayout(direction);
}
@Method()

View file

@ -120,7 +120,7 @@ export class DefaultActivityTemplate {
const iconCssClass = this.displayTypeIsPicker ? 'px-2' : 'px-4';
if (!icon)
return <div class={iconCssClass}></div>;
return undefined;
return (
<div class={`${iconCssClass} py-1`}>

View file

@ -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,

View file

@ -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;
}
@ -511,13 +512,6 @@ export class FlowchartComponent implements ContainerActivityComponent {
return this.activity;
const activity = this.activityLookup[currentItem.activityId] as Flowchart;
if (activity == null) {
alert("Critical");
location.reload();
return;
}
const activityDescriptor = descriptorsStore.activityDescriptors.find(x => x.typeName == activity.type);
if (activityDescriptor.isContainer)
@ -718,9 +712,6 @@ export class FlowchartComponent implements ContainerActivityComponent {
};
private onNavigateHierarchy = async (e: CustomEvent<FlowchartNavigationItem>) => {
debugger;
const item = e.detail;
const activityId = item.activityId;
let activity = this.activityLookup[activityId];

View file

@ -5,8 +5,6 @@ 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,
@ -70,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: {
@ -91,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({
@ -282,4 +291,4 @@ export function createGraph(
});
return graph;
};
};

View file

@ -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',

View file

@ -114,8 +114,8 @@ export class ActivityNodeShape extends Shape.HTML {
ActivityNodeShape.config({
ports: {
groups: {
in: {
position: 'dynamicIn',
left: {
position: 'left',
attrs: {
circle: {
r: 5,
@ -135,15 +135,57 @@ ActivityNodeShape.config({
},
},
},
out: {
position: 'dynamicOut',
right: {
position: 'right',
attrs: {
circle: {
r: 5,
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: 5,
magnet: true,
stroke: '#3c82f6',
strokeWidth: 2,
fill: '#fff',
},
text: {
fontSize: 12,
fill: '#888',
},
},
label: {
position: {
name: 'outside',
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 5,
magnet: true,
stroke: '#3c82f6',
strokeWidth: 2,
fill: '#fff',
},
text: {
fontSize: 12,

View file

@ -32,6 +32,8 @@ export class WorkflowDefinitionEditor {
private canvas: HTMLElsaCanvasElement;
private container: HTMLDivElement;
private toolbox: HTMLElsaWorkflowDefinitionEditorToolboxElement;
private readonly emitActivityChangedDebounced: (e: ActivityPropertyChangedEventArgs) => void;
private readonly updateModelDebounced: () => void;
private readonly saveChangesDebounced: () => void;
private readonly workflowDefinitionApi: WorkflowDefinitionsApi;
@ -40,6 +42,8 @@ export class WorkflowDefinitionEditor {
this.pluginRegistry = Container.get(PluginRegistry);
this.activityNameFormatter = Container.get(ActivityNameFormatter);
this.portProviderRegistry = Container.get(PortProviderRegistry);
this.emitActivityChangedDebounced = debounce(this.emitActivityChanged, 100);
this.updateModelDebounced = debounce(this.updateModel, 10);
this.saveChangesDebounced = debounce(this.saveChanges, 1000);
this.workflowDefinitionApi = Container.get(WorkflowDefinitionsApi);
}
@ -110,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.
@ -225,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<ActivityUpdatedArgs>) => {
await this.canvas.updateActivity({
@ -237,12 +240,12 @@ export class WorkflowDefinitionEditor {
});
await this.updateModel();
await this.emitActivityChanged(e.detail.activity, e.detail.propertyName);
this.emitActivityChangedDebounced({...e.detail, workflowEditor: this.el});
this.saveChangesDebounced();
}
private onWorkflowPropsUpdated = async (e: CustomEvent<WorkflowDefinitionPropsUpdatedArgs>) => {
await this.updateModel();
private onWorkflowPropsUpdated = (e: CustomEvent<WorkflowDefinitionPropsUpdatedArgs>) => {
this.updateModelDebounced();
this.saveChangesDebounced();
}

View file

@ -8,13 +8,16 @@ export class Toolbar {
public zoomToFit: () => Promise<void>;
@Prop()
public autoLayout: () => Promise<void>;
public autoLayout: (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
render() {
return (
<div class="elsa-panel-toolbar flex justify-center absolute border-b border-gray-200 top-0 px-1 pl-4 pb-2 text-sm bg-white z-10 space-x-2">
<button class="btn btn-primary" onClick={this.autoLayout}>
Auto-Layout
<button class="btn btn-primary" onClick={() => this.autoLayout("LR")}>
Auto-Layout (LR)
</button>
<button class="btn btn-primary" onClick={() => this.autoLayout("TB")}>
Auto-Layout (TB)
</button>
<button onClick={this.zoomToFit}class="btn btn-primary">
Zoom to fit

View file

@ -101,7 +101,6 @@ export class WorkflowDefinitionsPlugin implements Plugin {
private generateUniqueActivityName = async (activityDescriptor: ActivityDescriptor): Promise<string> => await generateUniqueActivityName([], activityDescriptor);
private saveWorkflowDefinition = async (definition: WorkflowDefinition, publish: boolean): Promise<WorkflowDefinition> => {
debugger;
const updatedWorkflow = await this.workflowDefinitionManager.saveWorkflow(definition, publish);
let reload = false;

View file

@ -10,7 +10,7 @@ export interface ContainerActivityComponent {
export(): Promise<Activity>
import(root: Activity): Promise<void>;
zoomToFit(): Promise<void>;
autoLayout(): Promise<void>;
autoLayout(direction: "TB" | "BT" | "LR" | "RL"): Promise<void>;
scrollToStart(): Promise<void>;
reset(): Promise<void>;
getCurrentLevel(): Promise<Activity>;