Creating workflow editor with Angular
Siarhei Huzarevich
Posted on July 15, 2024
In this tutorial, we will look at the process of creating a visual flow call editor using Angular and the @foblex/flow library.
The first step is to create a new Angular project:
ng new call-workflow-editor
To work with a graph, install @foblex/flow:
npm install @foblex/flow
Defining Models
To manage the components of a workflow, it is necessary to define models:
export interface IFlowModel {
nodes: INodeModel[];
connections: IConnectionModel[];
}
export interface INodeModel {
key: string;
name: string;
outputs: string[];
input?: string;
position: { x: number, y: number };
type: ENodeType;
}
export interface IConnectionModel {
key: string;
from: string;
to: string;
}
Workflow elements configuration
To control the behaviour of our visual workflow call editor, we introduce an enumeration of node types (ENodeType). Our application will use five main types of nodes:
Incoming Call: designed for incoming calls. This is the first node that is activated when a call is received.
Play Text: Allows you to play a preset text to the caller. Can be used for autoresponders or informational messages.
User Input: Waits for input from the user, such as pressing buttons on a phone to navigate through a menu.
To Operator: forwards the call to an operator. Used to contact a live operator if automated options are not suitable.
Disconnect: Ends the call. Can be used after all necessary actions have been completed or if the caller has decided to end the call.
Each node type is assigned unique settings, including name, icon, color, and number of outputs. This allows you to visually distinguish nodes from each other and configure their functionality:
export enum ENodeType {
IncomingCall = 'incoming-call',
PlayText = 'play-text',
UserInput = 'user-input',
ToOperator = 'to-operator',
Disconnect = 'disconnect'
}
export const NODE_MAP = {
[ ENodeType.IncomingCall ]: {
name: 'Incoming call',
icon: 'add_call',
color: '#39b372',
outputs: 1
},
[ ENodeType.UserInput ]: {
name: 'User Input',
icon: 'call_log',
color: '#2676ff',
outputs: 3
},
[ ENodeType.PlayText ]: {
name: 'Play text',
icon: 'wifi_calling_3',
color: '#AF94FF',
outputs: 1
},
[ ENodeType.ToOperator ]: {
name: 'To operator',
icon: 'wifi_calling_3',
color: '#ffb62a',
outputs: 1
},
[ ENodeType.Disconnect ]: {
name: 'Disconnect',
icon: 'phone_disabled',
color: '#ff859b',
outputs: 0
},
};
Editor component
Let's create a component for the visual editor, which will be responsible for visualization and interaction with the workflow:
ng generate component workflow-editor
In this component we will define the workflow display logic and user interaction.
Flow visualization
Displaying call flows is a key part of the editor. To do this, we use the components of the @foblex/flow library:
@Component({
selector: 'workflow-editor',
templateUrl: './workflow-editor.component.html',
styleUrls: [ './workflow-editor.component.scss' ],
})
export class WorkflowEditorComponent {
public flow: IFlowModel = {
nodes: [],
connections: []
};
public eConnectableSide = EFConnectableSide;
public cBehavior: EFConnectionBehavior = EFConnectionBehavior.FIXED;
public cType: EFConnectionType = EFConnectionType.SEGMENT;
}
@if(flow) {
<f-flow fDraggable>
<f-canvas fZoom>
@for (connection of flow.connections;track connection.key) {
<f-connection [fBehavior]="cBehavior"
[fType]="cType"
[fOutputId]="connection.from" [fInputId]="connection.to">
</f-connection>
}
@for (node of flow.nodes;track node.key) {
<div fNode fNodeInput [fInputId]="node.input"
[fInputDisabled]="!node.input"
[fInputConnectableSide]="eConnectableSide.TOP"
[fNodePosition]="node.position">
<div>{{ node.name }}</div>
@for (output of node.outputs;track output) {
<div fNodeOutput [fOutputId]="output" [fOutputConnectableSide]="eConnectableSide.BOTTOM">
}
</div>
}
</f-canvas>
</f-flow>
}
This will allow us to display the flow, but we need to add the ability to add nodes and connections, since we have nothing to display yet.
Adding new nodes
To make our visual flow call editor more interactive and functional, we provide the user with the ability to add new nodes. This allows you to create more complex and varied call processing scenarios.
public possibleNodes = Object.keys(NODE_MAP).map((key: string) => {
return {
...NODE_MAP[ key ],
type: key
}
});
public onCreateNode(event: FCreateNodeEvent): void {
const outputsCount = NODE_MAP[ event.data ].outputs;
const outputs = Array.from({ length: outputsCount }).map(() => {
return this.generateId();
});
this.flow.nodes.push({
key: this.generateId(),
name: NODE_MAP[ event.data ].name,
outputs: outputs,
position: event.rect,
type: event.data,
});
}
private generateId(): string {
return `${ Math.random().toString(36).substr(2, 9) }`;
}
Let's add this next to the flow component and add an event to the flow component:
@for (item of possibleNodes;track item) {
<button fExternalItem
[style.color]="item.color"
[fData]="item.type">
{{ item.icon }}
</button>
}
@if(flow) {
<f-flow (fCreateNode)="onCreateNode($event)">
...flow content
</f-flow>
}
Users can select nodes from a list and drag them onto the editor workspace using the fExternalItem directive. This action initiates the creation of a new node in the flow with the appropriate visualization and functionality settings.
Editing connections
After adding nodes to the flow, the next important step is creating connections between them. Connections determine the logic of transition from one node to another and form the final flow of call processing.
To edit and create new connections, we have provided the ability to visually interact with node components:
<f-flow (fCreateConnection)="onCreateConnection($event)"
(fReassignConnection)="onReassignConnection($event)">
<f-canvas fZoom>
<f-connection-for-create></f-connection-for-create>
...flow content
</f-canvas>
</f-flow>
public onCreateConnection(event: FCreateConnectionEvent): void {
const connection: IConnectionViewModel = {
from: event.fOutputId,
to: event.fInputId
};
this.flow.connections.push(connection);
}
public onReassignConnection(event: FReassignConnectionEvent): void {
const connection = this.flow.connections.find(c => c.from === event.fOutputId && c.to === event.oldFInputId);
if (connection) {
connection.to = event.newFInputId;
}
}
This allows users to easily modify an existing flow by adding new logical connections or changing existing ones. This approach makes the process of setting up flow calls flexible and intuitive.
Posted on July 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.