Reduce activity "jumpiness" on designer when adding and deleting activities

This commit is contained in:
Sipke Schoorstra 2021-07-20 12:47:37 +02:00
parent aafd173857
commit 20406c5e39
2 changed files with 90 additions and 67 deletions

View file

@ -109,40 +109,44 @@ export class ElsaWorkflowDesigner {
componentDidLoad() {
this.svgD3Selected = d3.select(this.svg);
this.innerD3Selected = d3.select(this.inner);
this.tryRerenderTree(1200);
this.tryRerenderTree(500);
}
componentWillRender() {
if(!!this.activityDisplayContexts)
if (!!this.activityDisplayContexts)
return;
const activityModels = this.workflowModel.activities;
const displayContexts: Map<ActivityDesignDisplayContext> = {};
const activityDescriptors: Array<ActivityDescriptor> = state.activityDescriptors;
for (const model of activityModels) {
const descriptor = activityDescriptors.find(x => x.type == model.type);
const description = model.description;
const bodyText = description && description.length > 0 ? description : undefined;
const bodyDisplay = bodyText ? `<p>${bodyText}</p>` : undefined;
const color = (descriptor.traits &= ActivityTraits.Trigger) == ActivityTraits.Trigger ? 'rose' : 'sky';
const displayName = model.displayName;
const displayContext: ActivityDesignDisplayContext = {
activityModel: model,
activityDescriptor: descriptor,
activityIcon: <ActivityIcon color={color}/>,
bodyDisplay: bodyDisplay,
displayName: displayName,
outcomes: [...model.outcomes],
};
eventBus.emit(EventTypes.ActivityDesignDisplaying, this, displayContext);
displayContexts[model.activityId] = displayContext;
displayContexts[model.activityId] = this.getActivityDisplayContext(model);
}
this.activityDisplayContexts = displayContexts;
}
getActivityDisplayContext(activityModel: ActivityModel) : ActivityDesignDisplayContext{
const activityDescriptors: Array<ActivityDescriptor> = state.activityDescriptors;
const descriptor = activityDescriptors.find(x => x.type == activityModel.type);
const description = activityModel.description;
const bodyText = description && description.length > 0 ? description : undefined;
const bodyDisplay = bodyText ? `<p>${bodyText}</p>` : undefined;
const color = (descriptor.traits &= ActivityTraits.Trigger) == ActivityTraits.Trigger ? 'rose' : 'sky';
const displayName = activityModel.displayName;
const displayContext: ActivityDesignDisplayContext = {
activityModel: activityModel,
activityDescriptor: descriptor,
activityIcon: <ActivityIcon color={color}/>,
bodyDisplay: bodyDisplay,
displayName: displayName,
outcomes: [...activityModel.outcomes],
};
eventBus.emit(EventTypes.ActivityDesignDisplaying, this, displayContext);
return displayContext;
}
showActivityEditorInternal(activity: ActivityModel, animate: boolean) {
eventBus.emit(EventTypes.ShowActivityEditor, this, activity, animate);
@ -201,39 +205,27 @@ export class ElsaWorkflowDesigner {
// Remove activity (will also remove its connections).
workflowModel = removeActivity(workflowModel, activity.activityId);
// For each incoming activity, try to connect it to a outgoing activity based on outcome.
for (const incomingConnection of incomingConnections) {
const incomingActivity = findActivity(workflowModel, incomingConnection.sourceId);
const outgoingConnection = outgoingConnections.find(x => x.outcome === incomingConnection.outcome);
// For each incoming activity, try to connect it to an outgoing activity based on outcome.
if (outgoingConnections.length > 0 && incomingConnections.length > 0) {
for (const incomingConnection of incomingConnections) {
const incomingActivity = findActivity(workflowModel, incomingConnection.sourceId);
let outgoingConnection = outgoingConnections.find(x => x.outcome === incomingConnection.outcome);
if (outgoingConnection) workflowModel = addConnection(workflowModel, incomingActivity.activityId, outgoingConnection.targetId, incomingConnection.outcome);
// If not matching outcome was found, pick the first one. The user will have to manually reconnect to the desired outcome.
if (!outgoingConnection)
outgoingConnection = outgoingConnections[0];
if (!!outgoingConnection)
workflowModel = addConnection(workflowModel, {
sourceId: incomingActivity.activityId,
targetId: outgoingConnection.targetId,
outcome: incomingConnection.outcome
});
}
}
this.updateWorkflowModel(workflowModel);
}
onActivityPicked = async args => {
const activityDescriptor = args as ActivityDescriptor;
const activityModel = this.newActivity(activityDescriptor);
this.addingActivity = true;
this.showActivityEditorInternal(activityModel, false);
};
onUpdateActivity = args => {
const activityModel = args as ActivityModel;
if (this.addingActivity) {
console.debug(`adding activity with ID ${activityModel.activityId}`)
const connectFromRoot = !this.parentActivityOutcome || this.parentActivityOutcome == '';
const sourceId = connectFromRoot ? null : this.parentActivityId;
const targetId = connectFromRoot ? this.parentActivityId : null;
this.addActivity(activityModel, sourceId, targetId, this.parentActivityOutcome);
this.addingActivity = false;
} else {
console.debug(`updating activity with ID ${activityModel.activityId}`)
this.updateActivity(activityModel);
}
};
newActivity(activityDescriptor: ActivityDescriptor): ActivityModel {
const activity: ActivityModel = {
activityId: uuid(),
@ -266,6 +258,7 @@ export class ElsaWorkflowDesigner {
outcome = outcome || 'Done';
const workflowModel = {...this.workflowModel, activities: [...this.workflowModel.activities, activity]};
const activityDisplayContext = this.getActivityDisplayContext(activity);
if (targetActivityId) {
const existingConnection = workflowModel.connections.find(x => x.targetId == targetActivityId && x.outcome == outcome);
@ -288,15 +281,25 @@ export class ElsaWorkflowDesigner {
const existingConnection = workflowModel.connections.find(x => x.sourceId == sourceActivityId && x.outcome == outcome);
if (existingConnection != null) {
// Remove the existing connection
workflowModel.connections = workflowModel.connections.filter(x => x != existingConnection);
const replacementConnection = {
...existingConnection,
// Create a new outbound connection between the source and the added activity.
const newOutboundConnection: ConnectionModel = {
sourceId: existingConnection.sourceId,
targetId: activity.activityId,
outcome: existingConnection.outcome
};
workflowModel.connections.push(newOutboundConnection);
// Create a new outbound activity between the added activity and the target of the source activity.
const connection: ConnectionModel = {
sourceId: activity.activityId,
targetId: existingConnection.targetId,
outcome: activityDisplayContext.outcomes[0]
};
workflowModel.connections.push(replacementConnection);
const connection: ConnectionModel = {sourceId: activity.activityId, targetId: existingConnection.targetId, outcome};
workflowModel.connections.push(connection);
} else {
const connection: ConnectionModel = {sourceId: sourceActivityId, targetId: activity.activityId, outcome: outcome};
@ -384,7 +387,7 @@ export class ElsaWorkflowDesigner {
// Connections between activities and their outcomes.
const activityDisplayContexts = this.activityDisplayContexts || {};
this.workflowModel.activities.forEach(activity => {
this.graph.setNode(activity.activityId, this.createActivityOptions(activity));
const displayContext = activityDisplayContexts[activity.activityId] || undefined;
@ -413,6 +416,30 @@ export class ElsaWorkflowDesigner {
});
}
onActivityPicked = async args => {
const activityDescriptor = args as ActivityDescriptor;
const activityModel = this.newActivity(activityDescriptor);
this.addingActivity = true;
this.showActivityEditorInternal(activityModel, false);
};
onUpdateActivity = args => {
const activityModel = args as ActivityModel;
if (this.addingActivity) {
console.debug(`adding activity with ID ${activityModel.activityId}`)
const connectFromRoot = !this.parentActivityOutcome || this.parentActivityOutcome == '';
const sourceId = connectFromRoot ? null : this.parentActivityId;
const targetId = connectFromRoot ? this.parentActivityId : null;
this.addActivity(activityModel, sourceId, targetId, this.parentActivityOutcome);
this.addingActivity = false;
} else {
console.debug(`updating activity with ID ${activityModel.activityId}`)
this.updateActivity(activityModel);
}
};
tryRerenderTree(waitTime?: number, attempt?: number) {
const maxTries = 3;
@ -433,19 +460,20 @@ export class ElsaWorkflowDesigner {
renderNodes() {
const prevTransform = this.innerD3Selected.attr('transform');
const scaleAfter = this.zoomParams.scale;
const root = d3.select(this.el);
this.svgD3Selected.call(this.zoom.scaleTo, 1);
this.dagreD3Renderer(this.innerD3Selected as any, this.graph as any);
this.svgD3Selected.call(this.zoom.scaleTo, scaleAfter);
this.innerD3Selected.attr('transform', prevTransform);
if (this.mode == WorkflowDesignerMode.Edit) {
d3.selectAll('.node.add').each((n: any) => {
root.selectAll('.node.add').each((n: any) => {
const node = this.graph.node(n) as any;
d3.select(node.elem)
.on('click', e => {
e.preventDefault();
d3.selectAll('.node.add svg').classed('elsa-text-green-400', false).classed('elsa-text-gray-400', true).classed('hover:elsa-text-blue-500', true);
root.selectAll('.node.add svg').classed('elsa-text-green-400', false).classed('elsa-text-gray-400', true).classed('hover:elsa-text-blue-500', true);
this.parentActivityId = node.activity.activityId;
this.parentActivityOutcome = node.outcome;
@ -465,14 +493,14 @@ export class ElsaWorkflowDesigner {
});
});
d3.selectAll('.node.start').each((n: any) => {
root.selectAll('.node.start').each((n: any) => {
const node = this.graph.node(n) as any;
d3.select(node.elem).on('click', e => {
this.showActivityPicker();
});
});
d3.selectAll('.edgePath').append(appendClickableEl).attr('class', 'label-clickable');
root.selectAll('.edgePath').append(appendClickableEl).attr('class', 'label-clickable');
function appendClickableEl() {
const originalD = this.querySelector('.path').getAttribute('d');
@ -481,7 +509,7 @@ export class ElsaWorkflowDesigner {
return this.appendChild(newPath);
}
d3.selectAll('.edgePath').each((edg: any) => {
root.selectAll('.edgePath').each((edg: any) => {
const edge = this.graph.edge(edg) as any;
d3.select(edge.elem).on('contextmenu', e => {
e.preventDefault();
@ -494,7 +522,7 @@ export class ElsaWorkflowDesigner {
});
}
d3.selectAll('.node.activity').each((n: any) => {
root.selectAll('.node.activity').each((n: any) => {
const node = this.graph.node(n) as any;
const activity = node.activity;
const activityId = activity.activityId;

View file

@ -88,12 +88,7 @@ export function findActivity(workflowModel: WorkflowModel, activityId: string) {
return workflowModel.activities.find(x => x.activityId === activityId);
}
export function addConnection(workflowModel: WorkflowModel, connection: ConnectionModel);
export function addConnection(workflowModel: WorkflowModel, sourceId: string, targetId: string, outcome: string);
export function addConnection(workflowModel: WorkflowModel, ...args: any) {
const connection = typeof (args) == 'object' ? args as ConnectionModel : {sourceId: args[0], targetId: args[1], outcome: args[3]};
export function addConnection(workflowModel: WorkflowModel, connection: ConnectionModel){
return {
...workflowModel,
connections: [...workflowModel.connections, connection]