Development

Development

Create General Plugin

With erxes, you can create your own plugins or extend the existing ones, which would help you to enhance your experience...

Create Integration plugin

Integration is the extent of the Inbox plugin, which allows third party softwares to be integrated to your shared Inbox.

Create General Plugin

With erxes, you can create your own plugins or extend the existing ones, which would help you to enhance your experience and increase your revenue by adding the value on your products/services or selling it on our our marketplace. This guideline will help you to develop your own plugins.

Installing erxes


Please go to the installation guideline to install erxes XOS, but no need to run the erxes with the same direction.

Plugin API

Plugin development in API part requires the following software prerequisites to be already installed on your computer.

Typescript

TypeScript is a popular choice for programmers accustomed to other languages with static typing, such as C# and Java.

Read more

Graphql

GraphQL is a query language for your API, and a server-side runtime for executing queries using a type system you define for your data.

Read more

Express.js

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

Read more

Mongodb

MongoDB empowers innovators to create, transform, and disrupt industries by unleashing the power of software and data.

Read more

Redis

Redis is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine.

Read more

RabbitMQ

RabbitMQ is a message-queueing software also known as a message broker or queue manager.

Read more

Plugin UI

Plugin development in UI part requires the following software prerequisites to be already installed on your computer.

Typescript

TypeScript is a popular choice for programmers accustomed to other languages with static typing, such as C# and Java.

Read more

Webpack

Webpack is a module bundler. Webpack can take care of bundling alongside a separate task runner.

Read more

ReactJS

React lets you build user interfaces out of individual pieces called components.

Read more

Creating New Plugin

Each plugin is composed of two parts, API and UI

  • Create new folders for both using the following command.
    cd erxes
    yarn create-plugin 

The command above starts CLI, prompting for few questions to create a new plugin as shown below. In this example we create plugin named document.

The example below is a new plugin, created from an example template, placed at the main navigation.

Creating from an empty template will result in as shown below, as we give you the freedom and space to develop your own plugin on erxes.

API file structure

After creating a plugin, the following files are generated automatically in your new plugin API.

πŸ“¦plugin-document-api
 ┣ πŸ“‚src
 ┃ ┣ πŸ“‚graphql
 ┃ ┃ ┣ πŸ“‚resolvers 
 ┃ ┃ ┃ ┣ index.ts
 ┃ ┃ ┃ ┣ mutations.ts
 ┃ ┃ ┃ β”— queries.ts
 ┃ ┃ ┣ index.ts
 ┃ ┃ β”— typeDefs.ts
 ┃ ┣ configs.ts
 ┃ ┣ messageBroker.ts
 ┃ β”— models.ts
 ┣ .env.sample
 ┣ package.json
 β”— tsconfig.json

Main files

Following files are generated automatically in plugin-[pluginName]-api/src.

configs.ts

This file contains main configuration of a plugin.

configs.ts file:

// path: ./packages/plugin-[pluginName]-api/src/configs.ts 
import typeDefs from './graphql/typeDefs';
import resolvers from './graphql/resolvers';
import { initBroker } from './messageBroker';
export let mainDb;
export let debug;
export let graphqlPubsub;
export let serviceDiscovery;
export default {
  name: '[pluginName]',
  graphql: async sd => {
    serviceDiscovery = sd;
    return {
      typeDefs: await typeDefs(sd),
      resolvers: await resolvers(sd)
    };
  },
  apolloServerContext: async (context) => {
    return context;
  },
  onServerInit: async options => {
    mainDb = options.db;
    initBroker(options.messageBrokerClient);
    graphqlPubsub = options.pubsubClient;
    debug = options.debug;
  }
}

messageBroker.ts

This file uses for connect with other plugins. You can see message broker functions from Common functions.

messageBroker.ts file:

// path: ./packages/plugin-[pluginName]-api/src/messageBroker.ts 
import { ISendMessageArgs, sendMessage } from "@erxes/api-utils/src/core";
import { serviceDiscovery } from "./configs";
import { Documents } from "./models";
let client;
export const initBroker = async cl => {
  client = cl;
  const { consumeQueue, consumeRPCQueue } = client;
  consumeQueue('document:send', async ({ data }) => {
    Documents.send(data);
    return {
      status: 'success',
    };
  });
  consumeRPCQueue('document:find', async ({ data }) => {
    return {
      status: 'success',
      data: await Documents.find({})
    };
  });
};
export const sendCommonMessage = async (
  args: ISendMessageArgs & { serviceName: string }
) => {
  return sendMessage({
    serviceDiscovery,
    client,
    ...args
  });
};
export default function() {
  return client;
}

GraphQL development

Inside packages/plugin-<new_plugin>-api/src, we have a graphql folder. The folder contains code related to GraphQL.

 πŸ“‚src
 ┣ πŸ“‚graphql
 ┃ ┣ πŸ“‚resolvers 
 ┃ ┃ ┣ index.ts
 ┃ ┃ ┣ mutations.ts
 ┃ ┃ β”— queries.ts
 ┃ ┣ index.ts
 ┃ β”— typeDefs.ts

GraphQL resolvers

Inside /graphql/resolvers/mutations GraphQL mutation codes.

mutation examples:

import { Documents, Types } from '../../models';
import { IContext } from "@erxes/api-utils/src/types"
const documentMutations = {
/**
 * Creates a new document
 */
async documentsAdd(_root, doc, _context: IContext) {
  return Documents.createDocument(doc);
},
/**
 * Edits a new document
 */
async documentsEdit(
  _root,
  { _id, ...doc },
  _context: IContext
) {
  return Documents.updateDocument(_id, doc);
},
/**
 * Removes a single document
 */
async documentsRemove(_root, { _id }, _context: IContext) {
  return Documents.removeDocument(_id);
},
/**
 * Creates a new type for document
 */
async documentTypesAdd(_root, doc, _context: IContext) {
  return Types.createType(doc);
},
async documentTypesRemove(_root, { _id }, _context: IContext) {
  return Types.removeType(_id);
},
async documentTypesEdit(
  _root,
  { _id, ...doc },
  _context: IContext
) {
return Types.updateType(_id, doc);
}
};
export default documentMutations;

Inside /graphql/resolvers/queries folder contains GraphQL query codes.

query examples:

import { Documents, Types } from "../../models";
import { IContext } from "@erxes/api-utils/src/types"
const documentQueries = {
  documents(
    _root,
    {
      typeId
    },
    _context: IContext
  ) {
    const selector: any = {};
    if (typeId) {
      selector.typeId = typeId;
    }
    return Documents.find(selector).sort({ order: 1, name: 1 });
  },
  documentTypes(_root, _args, _context: IContext) {
    return Types.find({});
  },
  documentsTotalCount(_root, _args, _context: IContext) {
    return Documents.find({}).countDocuments();
  }
};
export default documentQueries;

GraphQL typeDefs

Inside /graphql/typeDefs.ts file contains GraphQL typeDefs.

typeDefs:

import { gql } from 'apollo-server-express';
const types = `
  type Document {
    _id: String!
    name: String
    createdAt:Date
    expiryDate:Date
    checked:Boolean
    typeId: String
  
    currentType: DocumentType
  }
  type DocumentType {
    _id: String!
    name: String
  }
`;
const queries = `
  documents(typeId: String): [Document]
  documentTypes: [DocumentType]
  documentsTotalCount: Int
`;
const params = `
  name: String,
  expiryDate: Date,
  checked: Boolean,
  typeId:String
`;
const mutations = `
  documentsAdd(${params}): Document
  documentsRemove(_id: String!): JSON
  documentsEdit(_id:String!, ${params}): Document
  documentTypesAdd(name:String):DocumentType
  documentTypesRemove(_id: String!):JSON
  documentTypesEdit(_id: String!, name:String): DocumentType
`;
const typeDefs = async _serviceDiscovery => {
  return gql`
    scalar JSON
    scalar Date
    ${types}
    
    extend type Query {
      ${queries}
    }
    
    extend type Mutation {
      ${mutations}
    }
  `;
};
export default typeDefs;

Database development Inside packages/plugin-<new_plugin>-api/src, we have a models file. The file contains code related to MongoDB and mongoose.

πŸ“‚src
β”— models.ts

Mongoose schema and model

Inside src/models.ts, file contains Mongoose schema and models.

Mongoose schema and model example:

import * as _ from 'underscore';
import { model } from 'mongoose';
import { Schema } from 'mongoose';
export const typeSchema = new Schema({
  name: String
});
export const documentSchema = new Schema({
  name: String,
  createdAt: Date,
  expiryDate: Date,
  checked: Boolean,
  typeId: String
});
export const loadTypeClass = () => {
  class Type {
    public static async getType(_id: string) {
      const type = await Types.findOne({ _id });
      if (!type) {
        throw new Error('Type not found');
      }
      return type;
    }
    // create type
    public static async createType(doc) {
      return Types.create({ ...doc });
    }
    // remove type
    public static async removeType(_id: string) {
      return Types.deleteOne({ _id });
    }
    public static async updateType(_id: string, doc) {
      return Types.updateOne({ _id }, { $set: { ...doc } });
    }
  }
  typeSchema.loadClass(Type);
  return typeSchema;
};
export const loadDocumentClass = () => {
  class Document {
    public static async getDocument(_id: string) {
      const document = await Documents.findOne({ _id });
      if (!document) {
        throw new Error('Document not found');
      }
      return document;
    }
    // create
    public static async createDocument(doc) {
      return Documents.create({
        ...doc,
        createdAt: new Date()
      });
    }
    // update
    public static async updateDocument (_id: string, doc) {
      await Documents.updateOne(
        { _id },
        { $set: { ...doc } }
      ).then(err => console.error(err));
    }
    // remove
    public static async removeDocument(_id: string) {
      return Documents.deleteOne({ _id });
    }
  }
documentSchema.loadClass(Document);
return documentSchema;
};
loadDocumentClass();
loadTypeClass();
// tslint:disable-next-line
export const Types = model<any, any>(
  'document_types',
  typeSchema
);
// tslint:disable-next-line
export const Documents = model<any, any>('documents', documentSchema);

UI file structure

After creating new plugin using yarn-create-plugin command, the following files are generated automatically in your new plugin UI.

πŸ“¦plugin-[pluginName]-ui
 ┣ πŸ“‚src
 ┃ ┣ πŸ“‚components
 ┃ ┃ ┣ Form.tsx
 ┃ ┃ ┣ List.tsx
 ┃ ┃ ┣ Row.tsx
 ┃ ┃ ┣ SideBar.tsx
 ┃ ┃ β”— TypeForm.tsx
 ┃ ┣ πŸ“‚containers
 ┃ ┃ ┣ List.tsx
 ┃ ┃ β”— SideBarList.tsx
 ┃ ┣ πŸ“‚graphql
 ┃ ┃ ┣ index.ts
 ┃ ┃ ┣ mutations.ts
 ┃ ┃ β”— queries.ts
 ┃ ┣ App.tsx
 ┃ ┣ configs.js
 ┃ ┣ generalRoutes.tsx
 ┃ ┣ index.js
 ┃ ┣ routes.tsx
 ┃ β”— types.ts

Main files

Following files are generated automatically in plugin-[pluginName]-ui/src.

configs.js

Following file contains the main configs of plugin.

configs.js file:

// path: ./packages/plugin-[pluginName]-ui/src/configs.js 
  module.exports = {
    name: '[pluginName]',
    port: 3017,
    scope: '[pluginName]',
    exposes: {
      './routes': './src/routes.tsx'
    },
    routes: {
      url: 'http://localhost:3017/remoteEntry.js',
      scope: '[pluginName]',
      module: './routes'
    },
    menus:[
        {
          "text":"[pluginName]",
          "url":"/[pluginUrl]",
          "icon":"icon-star",
          "location":"[mainNavigation or settings]"
        }
      ]
  };

routes.tsx

Following file contains routes of plugin UI.

routes.tsx file:

// path: ./packages/plugin-[pluginName]-ui/src/routes.tsx 
  import asyncComponent from '@erxes/ui/src/components/AsyncComponent';
  import queryString from 'query-string';
  import React from 'react';
  import { Route } from 'react-router-dom';
  const List = asyncComponent(() =>
    import(/* webpackChunkName: "List - Documents" */ './containers/List')
  );
  const documents = ({ location, history }) => {
    const queryParams = queryString.parse(location.search);
    const { type } = queryParams;
    return <List typeId={type} history={history} />;
  };
  const routes = () => {
    return <Route path="/documents/" component={documents} />;
  };

App.tsx

This file contains main component of application.

App.tsx file:

// path: ./packages/plugin-[pluginName]-ui/src/App.tsx 
  import React from 'react';
  import GeneralRoutes from './generalRoutes';
  import { PluginLayout } from '@erxes/ui/src/styles/main';
  const App = () => {
    return (
      <PluginLayout>
        <GeneralRoutes />
      </PluginLayout>
    );
  };
  export default App;

UI development

Components

Inside .src folder, we have a components folder. The folder contains main components of plugin.

 πŸ“‚src
 ┣ πŸ“‚components
 ┃ ┣ Form.tsx
 ┃ ┣ List.tsx
 ┃ ┣ Row.tsx
 ┃ ┣ SideBar.tsx
 ┃ β”— TypeForm.tsx

components example:

 // path: ./packages/plugin-[pluginName]-ui/src/components/TypeForm.tsx 
  import { __ } from '@erxes/ui/src/utils/core';
  import React from 'react';
  import { IType } from '../types';
  import { IButtonMutateProps, IFormProps } from '@erxes/ui/src/types';
  import Form from '@erxes/ui/src/components/form/Form';
  import {
    ControlLabel,
    FormControl,
    FormGroup
  } from '@erxes/ui/src/components/form';
  import Button from '@erxes/ui/src/components/Button';
  import { ModalFooter } from '@erxes/ui/src/styles/main';
  type Props = {
    renderButton: (props: IButtonMutateProps) => JSX.Element;
    closeModal?: () => void;
    afterSave?: () => void;
    remove?: (type: IType) => void;
    types?: IType[];
    type: IType;
  };
  const TypeForm = (props: Props) => {
    const { type, closeModal, renderButton, afterSave } = props;
    const generateDoc = (values: {
      _id?: string;
      name: string;
      content: string;
    }) => {
      const finalValues = values;
      const { type } = props;
      if (type) {
        finalValues._id = type._id;
      }
      return {
        ...finalValues
      };
    };
    const renderContent = (formProps: IFormProps) => {
      const { values, isSubmitted } = formProps;
      const object = type || ({} as any);
      return (
        <>
          <FormGroup>
            <ControlLabel required={true}>Todo Type</ControlLabel>
            <FormControl
              {...formProps}
              name='name'
              defaultValue={object.name}
              type='text'
              required={true}
              autoFocus={true}
            />
          </FormGroup>
          <ModalFooter id={'AddTypeButtons'}>
            <Button btnStyle='simple' onClick={closeModal} icon='times-circle'>
              Cancel
            </Button>
            {renderButton({
              passedName: 'type',
              values: generateDoc(values),
              isSubmitted,
              callback: closeModal || afterSave,
              object: type
            })}
          </ModalFooter>
        </>
      );
    };
    return <Form renderContent={renderContent} />;
  };
  export default TypeForm;

Containers

Inside .src folder, we have a containers folder. The folder contains a component that contains codes related to API.

 πŸ“‚src
  ┣ πŸ“‚containers
  ┃ ┣ List.tsx
  ┃ β”— SideBarList.tsx

containers example:

  // path: ./packages/plugin-[pluginName]-ui/src/containers/SideBarList.tsx 
  import gql from 'graphql-tag';
  import * as compose from 'lodash.flowright';
  import { graphql } from 'react-apollo';
  import { Alert, confirm, withProps } from '@erxes/ui/src/utils';
  import SideBar from '../components/SideBar';
  import {
    EditTypeMutationResponse,
    RemoveTypeMutationResponse,
    TypeQueryResponse
  } from '../types';
  import { mutations, queries } from '../graphql';
  import React from 'react';
  import { IButtonMutateProps } from '@erxes/ui/src/types';
  import ButtonMutate from '@erxes/ui/src/components/ButtonMutate';
  import Spinner from '@erxes/ui/src/components/Spinner';
  type Props = {
    history: any;
    currentTypeId?: string;
  };
  type FinalProps = {
    listTemplateTypeQuery: TypeQueryResponse;
  } & Props &
    RemoveTypeMutationResponse &
    EditTypeMutationResponse;
  const TypesListContainer = (props: FinalProps) => {
    const { listTemplateTypeQuery, typesEdit, typesRemove, history } = props;
    if (listTemplateTypeQuery.loading) {
      return <Spinner />;
    }
    // calls gql mutation for edit/add type
    const renderButton = ({
      passedName,
      values,
      isSubmitted,
      callback,
      object
    }: IButtonMutateProps) => {
      return (
        <ButtonMutate
          mutation={object ? mutations.editType : mutations.addType}
          variables={values}
          callback={callback}
          isSubmitted={isSubmitted}
          type="submit"
          successMessage={`You successfully ${
            object ? 'updated' : 'added'
          } a ${passedName}`}
          refetchQueries={['listTemplateTypeQuery']}
        />
      );
    };
    const remove = type => {
      confirm('You are about to delete the item. Are you sure? ')
        .then(() => {
          typesRemove({ variables: { _id: type._id } })
            .then(() => {
              Alert.success('Successfully deleted an item');
            })
            .catch(e => Alert.error(e.message));
        })
        .catch(e => Alert.error(e.message));
    };
    const updatedProps = {
      ...props,
      types: listTemplateTypeQuery.templateTypes || [],
      loading: listTemplateTypeQuery.loading,
      remove,
      renderButton
    };
    return <SideBar {...updatedProps} />;
  };
  export default withProps<Props>(
    compose(
      graphql(gql(queries.listTemplateTypes), {
        name: 'listTemplateTypeQuery',
        options: () => ({
          fetchPolicy: 'network-only'
        })
      }),
      graphql(gql(mutations.removeType), {
        name: 'typesRemove',
        options: () => ({
          refetchQueries: ['listTemplateTypeQuery']
        })
      })
    )(TypesListContainer)
  );

GraphQL

Inside .src folder, we have a graphql folder. The folder contains code related to GraphQL.

πŸ“‚src
 ┣ πŸ“‚graphql
 ┃ ┣ index.ts
 ┃ ┣ mutations.ts
 ┃ β”— queries.ts

Inside /graphql/mutations.ts GraphQL mutation codes.

GraphQL mutation examples:

const add = `
  mutation documentsAdd($name: String!, $expiryDate: Date, $typeId:String) {
    documentsAdd(name:$name, expiryDate: $expiryDate, typeId:$typeId) {
      name
      _id
      expiryDate
      typeId
    }
  }
`;

const remove = `
  mutation documentsRemove($_id: String!){
    documentsRemove(_id: $_id)
  }
  `;

const edit = `
  mutation documentsEdit($_id: String!, $name:String, $expiryDate:Date, $checked:Boolean, $typeId:String){
    documentsEdit(_id: $_id, name: $name, expiryDate:$expiryDate, checked:$checked, typeId:$typeId){
      _id
    }
  }
  `;

const addType = `
  mutation typesAdd($name: String!){
    documentTypesAdd(name:$name){
      name
      _id
    }
  }
  `;

const removeType = `
  mutation typesRemove($_id:String!){
    documentTypesRemove(_id:$_id)
  }
`;

const editType = `
  mutation typesEdit($_id: String!, $name:String){
    documentTypesEdit(_id: $_id, name: $name){
      _id
    }
  }
`;

export default {
  add,
  remove,
  edit,
  addType,
  removeType,
  editType
};

Inside /graphql/queries.ts GraphQL query codes.

GraphQL query examples:

const list = `
  query listQuery($typeId: String) {
    documents(typeId: $typeId) {
      _id
      name
      expiryDate
      createdAt
      checked
      typeId
      currentType{
        _id
        name
      }
    }
  }
`;
const listDocumentTypes = `
  query listDocumentTypeQuery{
    documentTypes{
      _id
      name
    }
  }
`;
const totalCount = `
  query documentsTotalCount{
    documentsTotalCount
  }
`;
export default {
  list,
  totalCount,
  listDocumentTypes
};

Configuring UI

Running port for plugin

Inside packages/plugin-<new_plugin>-ui/src/configs.js, running port for plugin UI is set as shown below. Default value is 3017. Please note that each plugin has to have its UI running on an unique port. You may need to change the port manually (inside configs.js) if developing multiple plugins.

module.exports = {
  name: 'new_plugin',
  port: 3017,
  scope: 'new_plugin',
  exposes: {
    './routes': './src/routes.tsx'
  },
  routes: {
    url: 'http://localhost:3017/remoteEntry.js',
    scope: 'new_plugin',
    module: './routes'
  },
  menus: []
};

Location for plugin

Inside packages/plugin-<new_plugin>-ui/src/configs.js, we have a configuration section. The example below places new plugin at the main navigation menu.

menus: [
  {
    text: 'New plugin',
    url: '/new_plugins',
    icon: 'icon-star',
    location: 'mainNavigation',
  }
]

If you want to place it only inside settings, example is illustrated below.

menus: [
  {
    text: 'New plugin',
    to: '/new_plugins',
    image: '/images/icons/erxes-18.svg',
    location: 'settings',
    scope: 'new_plugin'
  }
]

Enabling plugins

"plugins" section inside cli/configs.json contains plugin names that run when erxes starts. Please note to configure this section if you decide to enable other plugins, remove or recreate plugins.

{
 "jwt_token_secret": "token",
 "dashboard": {},
 "client_portal_domains": "",
 "elasticsearch": {},
 "redis": {
   "password": ""
 },
 "mongo": {
   "username": "",
   "password": ""
 },
 "rabbitmq": {
   "cookie": "",
   "user": "",
   "pass": "",
   "vhost": ""
 },
 "plugins": [
   {
     "name": "logs"
   },
   {
     "name": "new_plugin",
     "ui": "local"
   }
 ]
}

Running erxes

Please note that create-plugin command automatically adds a new line inside cli/configs.json, as well as installs the dependencies necessary.

{
    "jwt_token_secret": "token",
    "client_portal_domains": "",
    "elasticsearch": {},
    "redis": {
        "password": "pass"
    },
    "mongo": {
        "username": "",
        "password": ""
    },
    "rabbitmq": {
        "cookie": "",
        "user": "",
        "pass": "",
        "vhost": ""
    },
    "plugins": [
      {
        "name": "new_plugin",
        "ui": "local"
      }
    ]
}
  1. Run the following command
cd erxes/cli
yarn install
  1. Then run the following command to start erxes with your newly installed plugin
./bin/erxes.js dev

Create Integration plugin

Integration is the extent of the Inbox plugin, which allows third party softwares to be integrated to your shared Inbox.

So let's assume, you've already created your plugin by using the above guideline and the name of your plugin is IMAP.

Add the following Inbox-related plugins to configs.json and start the services.

 "plugins": [
        {
            "name": "forms",
            "ui": "remote"
        },
        {
            "name": "contacts",
            "ui": "local"
        },
        {
            "name": "inbox",
            "ui": "local"
        },
        {
            "name": "imap",
            "ui": "local"
        }

These are the core concepts of the inbox integration

  1. Brand - Biggest level of data seperation. Let's assume your company is a group company that consists of 3 child companies. In that case each brand will represent each child companies.
  2. Channel - Group of integrations and team members, which represents who is responsible for which integrations.
  3. Integration - In IMAP's case, a set of configs that includes email address, password, smtp host etc.
  4. Customer - In IMAP's case, the person to sent the email.
  5. Conversation - In IMAP's case, whole email thread.
  6. Conversation Messages - In IMAP's case, each email entry in single email thread.

Lifecycle of integration

  1. Create an integration instance with corresponing configs, which will be store in inbox's database and later you will use these to work with the apis you want to connect.
  2. Receive data from your desired apis using integration configs in plugin-"integration-name"-api.
  3. Store the data as conversations, conversation messages, and customers. You have to store conversations in inbox's and customers in contacts's database and you have to store conversation messages in your plugin's database.
  4. Once you stored the conversations and customers. It will show up in inbox's sidebar. But you will be responsive for the conversation detail in inbox's UI.
  5. Since you can show anything in conversation detail will also be responsible for further actions like sending response to customer.

Let's demonstrate above steps using IMAP as an example

Create an integration

Let's look at configs.js in plugin-imap-ui

 inboxIntegration: {
    name: 'IMAP',
    description:
      'Connect a company email address such as [email protected] or [email protected]',
    isAvailable: true,
    kind: 'imap',
    logo: '/images/integrations/email.png',
    createModal: 'imap',
    createUrl: '/settings/integrations/imap',
    category:
      'All integrations, For support teams, Marketing automation, Email marketing'
  }

It will create following in block in /settings/integrations location

"./inboxIntegrationForm": "./src/components/IntegrationForm.tsx",

and

inboxIntegrationForm: './inboxIntegrationForm',

these lines will show ./src/components/IntegrationForm.tsx component when you click on the add link in the above picture

When you click on the "Save" button, it will send the message to plugin-imap-api. So you have to write a consumer like the following

consumeRPCQueue(
  'imap:createIntegration',
  async ({ subdomain, data: { doc, integrationId } }) => {
    const models = await generateModels(subdomain);

    const integration = await models.Integrations.create({
      inboxId: integrationId,
      ...doc
    });

    await listenIntegration(subdomain, integration);

    await models.Logs.createLog({
      type: 'info',
      message: `Started syncing ${integration.user}`
    });

    return {
      status: 'success'
    };
  }
);

here is the example

Receive data from your desired APIs

here is the code example

Store the data

const apiCustomerResponse = await sendContactsMessage({
  subdomain,
  action: 'customers.createCustomer',
  data: {
    integrationId: integration.inboxId,
    primaryEmail: from
  },
  isRPC: true
});

it will send a createCustomer message to contacts plugin and contact plugin will store it in it's database.

const { _id } = await sendInboxMessage({
  subdomain,
  action: 'integrations.receive',
  data: {
    action: 'create-or-update-conversation',
    payload: JSON.stringify({
      integrationId: integration.inboxId,
      customerId,
      createdAt: msg.date,
      content: msg.subject
    })
  },
  isRPC: true
});

it will send a create or update conversation message to inbox plugin and inbox plugin will store it in it's database.

Conversation detail

in configs.js of plugin-imap-ui

"./inboxConversationDetail": "./src/components/ConversationDetail.tsx",

and

inboxConversationDetail: './inboxConversationDetail',

will render ./src/components/ConversationDetail.tsx component in conversation detail section of inbox ui

Creating new integration plugin

Each plugin is composed of two parts, API and UI

  1. Create new folders for both using the following command.
cd erxes
yarn create-plugin

The command above starts CLI, prompting for few questions to create a new integration plugin as shown below. In this example we create twitter integration.

The example below is a new integration plugin, created from an example template, placed at the settings integration.

The example below is a new integration plugins configuration, created from an example template, placed at the settings integrations config.

The example below is a creating new example integration using the form.

The example below is a creating new integration with detail page. If you choose with detail choice in integration plugin UI template. You will link to detail page when clicking add button.

API file structure

After creating an integration plugin, the following files are generated automatically in your integration plugin API.

πŸ“¦plugin-twitter-api
 ┣ πŸ“‚src
 ┃ ┣ πŸ“‚graphql
 ┃ ┃ ┣ πŸ“‚resolvers
 ┃ ┃ ┃ ┣ index.ts
 ┃ ┃ ┃ ┣ mutations.ts
 ┃ ┃ ┃ β”— queries.ts
 ┃ ┃ ┣ index.ts
 ┃ ┃ β”— typeDefs.ts
 ┃ ┣ configs.ts
 ┃ ┣ controller.ts
 ┃ ┣ messageBroker.ts
 ┃ β”— models.ts
 ┣ .env.sample
 ┣ package.json
 β”— tsconfig.json

Main files

Following files are generated automatically in plugin-[pluginName]-api/src.

configs.ts

This file contains main configuration of an integration plugin.

configs.ts file:

// path: ./packages/plugin-[pluginName]-api/src/configs.ts 
  import typeDefs from './graphql/typeDefs';
  import resolvers from './graphql/resolvers';
  import { initBroker } from './messageBroker';
  import init from './controller';
  export let mainDb;
  export let graphqlPubsub;
  export let serviceDiscovery;
  export let debug;
  export default {
    name: 'twitter',
    graphql: sd => {
      serviceDiscovery = sd;
      return {
        typeDefs,
        resolvers
      };
    },
    meta: {
      // this code will show the integration in UI settings -> integrations
      inboxIntegration: {
        kind: 'twitter',
        label: 'Twitter'
      }
    },
    apolloServerContext: async (context) => {
      return context;
    },
    onServerInit: async options => {
      const app = options.app;
      mainDb = options.db;
      debug = options.debug;
      graphqlPubsub = options.pubsubClient;
      initBroker(options.messageBrokerClient);
      // integration controller
      init(app);
    }
  };

controller.ts

This file contains integration controllers such as listening integration, connecting integration with erxes, and create conversation and customer. In this example shows message savings.

controller.ts file:

import { sendContactsMessage, sendInboxMessage } from './messageBroker';
  import { Customers, Messages } from './models';
  // util function
  const searchMessages = (linkedin, criteria) => {
    return new Promise((resolve, reject) => {
      const messages: any = [];
    });
  };
  // Example for save messages to inbox and create or update customer
  const saveMessages = async (
    linkedin,
    integration,
    criteria
  ) => {
    const msgs: any = await searchMessages(linkedin, criteria);
    for (const msg of msgs) {
      const message = await Messages.findOne({
        messageId: msg.messageId
      });
      if (message) {
        continue;
      }
      const from = msg.from.value[0].address;
      const prev = await Customers.findOne({ email: from });
      let customerId;
      if (!prev) {
        // search customer from contacts api
        const customer = await sendContactsMessage({
          subdomain: 'os',
          action: 'customers.findOne',
          data: {
            primaryEmail: from
          },
          isRPC: true
        });
        if (customer) {
          customerId = customer._id;
        } else {
          // creating new customer
          const apiCustomerResponse = await sendContactsMessage({
            subdomain: 'os',
            action: 'customers.createCustomer',
            data: {
              integrationId: integration.inboxId,
              primaryEmail: from
            },
            isRPC: true
          });
          customerId = apiCustomerResponse._id;
        }
        await Customers.create({
          inboxIntegrationId: integration.inboxId,
          contactsId: customerId,
          email: from
        });
      } else {
        customerId = prev.contactsId;
      }
      let conversationId;
      const relatedMessage = await Messages.findOne({
        $or: [
          { messageId: msg.inReplyTo },
          { messageId: { $in: msg.references || [] } },
          { references: { $in: [msg.messageId] } },
          { references: { $in: [msg.inReplyTo] } }
        ]
      });
      if (relatedMessage) {
        conversationId = relatedMessage.inboxConversationId;
      } else {
        const { _id } = await sendInboxMessage({
          subdomain: 'os',
          action: 'integrations.receive',
          data: {
            action: 'create-or-update-conversation',
            payload: JSON.stringify({
              integrationId: integration.inboxId,
              customerId,
              createdAt: msg.date,
              content: msg.subject
            })
          },
          isRPC: true
        });
        conversationId = _id;
      }
      await Messages.create({
        inboxIntegrationId: integration.inboxId,
        inboxConversationId: conversationId,
        createdAt: msg.date,
        messageId: msg.messageId,
        inReplyTo: msg.inReplyTo,
        references: msg.references,
        subject: msg.subject,
        body: msg.html,
        to: msg.to && msg.to.value,
        cc: msg.cc && msg.cc.value,
        bcc: msg.bcc && msg.bcc.value,
        from: msg.from && msg.from.value,
      });
    }
  };
  // controller for twitter
  const init = async app => {
    // write integration login method below
    app.get('/login', async (req, res) => {
      res.send("login")
    });
    
    app.post('/receive', async (req, res, next) => {
      try {
        // write receive message from integration code here
        res.send("Successfully receiving message");
      } catch (e) {
        return next(new Error(e));
      }
      res.sendStatus(200);
    });
  };
  export default init;

messageBroker.ts

This file uses for connect with other plugins. You can see message broker functions from Common functions.

messageBroker.ts file:

// path: ./packages/plugin-[pluginName]-api/src/messageBroker.ts 
  import * as dotenv from 'dotenv';
  import {
    ISendMessageArgs,
    sendMessage as sendCommonMessage
  } from '@erxes/api-utils/src/core';
  import { serviceDiscovery } from './configs';
  import { Customers, Integrations, Messages } from './models';
  dotenv.config();
  let client;
  export const initBroker = async cl => {
    client = cl;
    const { consumeRPCQueue } = client;
    consumeRPCQueue(
      'twitter:createIntegration',
      async ({ data: { doc, integrationId } }) => {
        await Integrations.create({
          inboxId: integrationId,
          ...(doc || {})
        });
        return {
          status: 'success'
        };
      }
    );
    consumeRPCQueue(
      'twitter:removeIntegration',
      async ({ data: { integrationId } }) => {
        await Messages.remove({ inboxIntegrationId: integrationId });
        await Customers.remove({ inboxIntegrationId: integrationId });
        await Integrations.remove({ inboxId: integrationId });
        return {
          status: 'success'
        };
      }
    );
  };
  export default function() {
    return client;
  }
  export const sendContactsMessage = (args: ISendMessageArgs) => {
    return sendCommonMessage({
      client,
      serviceDiscovery,
      serviceName: 'contacts',
      ...args
    });
  };
  export const sendInboxMessage = (args: ISendMessageArgs) => {
    return sendCommonMessage({
      client,
      serviceDiscovery,
      serviceName: 'inbox',
      ...args
    });
  };

GraphQL development

Inside packages/plugin-<new_plugin>-api/src, we have a graphql folder. The folder contains code related to GraphQL.

 πŸ“‚src
 ┣ πŸ“‚graphql
 ┃ ┣ πŸ“‚resolvers 
 ┃ ┃ ┣ index.ts
 ┃ ┃ ┣ mutations.ts
 ┃ ┃ β”— queries.ts
 ┃ ┣ index.ts
 ┃ β”— typeDefs.ts

GraphQL resolvers

Inside /graphql/resolvers/mutations GraphQL mutation codes.

mutation examples:

  import { Accounts } from '../../models';
  import { IContext } from "@erxes/api-utils/src/types"
  const twitterMutations = {
    async twitterAccountRemove(_root, {_id}: {_id: string}, _context: IContext) {
      await Accounts.removeAccount(_id);
      return 'deleted';
    }
  };
  export default twitterMutations;

Inside /graphql/resolvers/queries folder contains GraphQL query codes.

query examples:

  import { IContext } from '@erxes/api-utils/src/types';
  import { Accounts, Messages } from '../../models';
  const queries = {
    async twitterConversationDetail(
      _root,
      { conversationId },
      _context: IContext
    ) {
      const messages = await Messages.find({
        inboxConversationId: conversationId
      });
      const convertEmails = emails =>
        (emails || []).map(item => ({ name: item.name, email: item.address }));
      return messages.map(message => {
        return {
          _id: message._id,
          mailData: {
            messageId: message.messageId,
            from: convertEmails(message.from),
            to: convertEmails(message.to),
            cc: convertEmails(message.cc),
            bcc: convertEmails(message.bcc),
            subject: message.subject,
            body: message.body,
          }
        };
      });
    },
    async twitterAccounts(_root, _args, _context: IContext) {
      return Accounts.getAccounts();
    }
  };
  export default queries;

GraphQL typeDefs

Inside /graphql/typeDefs.ts file contains GraphQL typeDefs.

typeDefs:

import { gql } from 'apollo-server-express';
  const types = `
    type Twitter {
      _id: String!
      title: String
      mailData: JSON
    }
  `;
  const queries = `
    twitterConversationDetail(conversationId: String!): [Twitter]
    twitterAccounts: JSON
  `;
  const mutations = `
    twitterAccountRemove(_id: String!): String
  `;
  const typeDefs = gql`
    scalar JSON
    scalar Date
    ${types}
    extend type Query {
      ${queries}
    }
    extend type Mutation {
      ${mutations}
    }
  `;
  export default typeDefs;

Database development

Inside packages/plugin-<new_plugin>-api/src, we have a models file. The file contains code related to MongoDB and mongoose.

πŸ“‚src
β”— models.ts

Mongoose schema and model

Inside src/models.ts, file contains Mongoose schema and models.

Mongoose schema and model example:

1

UI file structure

Automatically generated integration plugin UI's file structure same as general plugin. Only difference is configuring UI. If you want to see general plugin UI file structure click here.

Configuring UI

Running port for plugin

Inside packages/plugin-<new_plugin>-ui/src/configs.js, running port for plugin UI is set as shown below. Default value is 3024. Please note that each plugin has to have its UI running on an unique port. You may need to change the port manually (inside configs.js) if developing multiple plugins.

module.exports = {
  name: 'twitter',
  scope: 'twitter',
  port: 3024,
  exposes: {
    './routes': './src/routes.tsx',

    // below components will work dynamically. In our case we call this components in inbox-ui.
    './inboxIntegrationSettings': './src/components/IntegrationSettings.tsx',
    './inboxIntegrationForm': './src/components/IntegrationForm.tsx',
    './inboxConversationDetail': './src/components/ConversationDetail.tsx'

  },
  routes: {
    url: 'http://localhost:3024/remoteEntry.js',
    scope: 'twitter',
    module: './routes'
  },

  // calling exposed components
  inboxIntegrationSettings: './inboxIntegrationSettings',
  inboxIntegrationForm: './inboxIntegrationForm',
  inboxConversationDetail: './inboxConversationDetail',

  inboxIntegration: {
    name: 'Twitter',

    // integration desciption will show in integration box.
    description:
      'Please write integration description on plugin config file',
    
    // this variable shows that integration available in production mode. In development mode we always set true value.
    isAvailable: true,

    // integration kind will useful for call integration form dynamically 
    kind: 'twitter',

    // integration logo
    logo: '/images/integrations/twitter.png',
    
    // if you choose with detail page when creating an integration plugin using cli. When you click add button in integration box, integration will link to this url.
    createUrl: '/settings/integrations/twitter',
  }
};

Plugins dynamic components

Files where exposed components are called dynamically in plugin inbox UI.

inboxIntegrationSettings.tsx component Inside packages/plugin-inbox-ui/src/settings/integrationsConfig/components/IntegrationConfigs.tsx, we have a loadDynamicComponent function. The code below shows the integrationConfigs component being called dynamically in the plugin inbox UI.

{loadDynamicComponent(
    'inboxIntegrationSettings',
    {
      renderItem: this.renderItem
    },
    true
  )}

inboxIntegrationForm.tsx component Inside packages/plugin-inbox-ui/src/settings/integrations/containers/common/IntegrationForm.tsx, we have a loadDynamicComponent function. The code below shows the integrationForm component being called dynamically in the plugin inbox UI.

return loadDynamicComponent(
  'inboxIntegrationForm',
  updatedProps,
  false,
  type
);

inboxConversationDetail.tsx component Inside packages/plugin-inbox-ui/src/inbox/components/conversationDetail/workarea/WorkArea.tsx, we have a loadDynamicComponent function. The code below shows the inboxConversationDetail component being called dynamically in the plugin inbox UI.

content = loadDynamicComponent('inboxConversationDetail', {
  ...this.props
});

Enabling plugins

"plugins" section inside cli/configs.json contains plugin names that run when erxes starts. Please note to configure this section if you decide to enable other plugins, remove or recreate plugins.

{
 "jwt_token_secret": "token",
 "dashboard": {},
 "client_portal_domains": "",
 "elasticsearch": {},
 "redis": {
   "password": ""
 },
 "mongo": {
   "username": "",
   "password": ""
 },
 "rabbitmq": {
   "cookie": "",
   "user": "",
   "pass": "",
   "vhost": ""
 },
 "plugins": [
   {
     "name": "logs"
   },
   {
     "name": "new_plugin",
     "ui": "local"
   }
 ]
}

Running erxes

Please note that create-plugin command automatically adds a new line inside cli/configs.json, as well as installs the dependencies necessary.

{
    "jwt_token_secret": "token",
    "client_portal_domains": "",
    "elasticsearch": {},
    "redis": {
        "password": "pass"
    },
    "mongo": {
        "username": "",
        "password": ""
    },
    "rabbitmq": {
        "cookie": "",
        "user": "",
        "pass": "",
        "vhost": ""
    },
    "plugins": [
        {
            "name": "forms",
            "ui": "remote"
        },
        {
            "name": "contacts",
            "ui": "local"
        },
        {
            "name": "inbox",
            "ui": "local"
        },
        {
            "name": "twitter",
            "ui": "local"
        },
    ]
}
  1. Run the following command
cd erxes/cli
yarn install
  1. Then run the following command to start erxes with your newly installed plugin
./bin/erxes.js dev

Was this page helpful?