Creating workflow editor with Angular

shuzarevich

Siarhei Huzarevich

Posted on July 15, 2024

Creating workflow editor with Angular

Demo
Library

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


Enter fullscreen mode Exit fullscreen mode

To work with a graph, install @foblex/flow:



npm install @foblex/flow


Enter fullscreen mode Exit fullscreen mode

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;
}


Enter fullscreen mode Exit fullscreen mode

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
  },
};


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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;
}



Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

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) }`;
}


Enter fullscreen mode Exit fullscreen mode

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>
}


Enter fullscreen mode Exit fullscreen mode

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.

Create nodes

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>


Enter fullscreen mode Exit fullscreen mode


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;
  }
}


Enter fullscreen mode Exit fullscreen mode

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.

Connect nodes

Example Library

💖 💪 🙅 🚩
shuzarevich
Siarhei Huzarevich

Posted on July 15, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related