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.
✋ Caution
- Before you start developing your own plugins, ensure there is no plugins with the same name or similar name in our marketplace that would bring any confusion as the name would be used many places starting from your
API
,GraphQL
,query
,mutation
, etc. - Name must be in small letters with no symbols and space in between.
- Name of All your
GraphQL
type,query
,mutation
must start with your plugin name. - Names of your database collection also must start with your plugin name.
- Name of your UIroutes or
url
-s also must be start with you pluging name.
Installing erxes
Please go to the installation guideline to install erxes XOS, but no need to run the erxes with the same direction.
❌ Danger
We assume you've already installed erxes XOS on your device. Otherwise the guideline below would not work out properly. Please make sure you should be back after you install erxes XOS using the installation guideline.
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.
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.
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.
Mongodb
MongoDB empowers innovators to create, transform, and disrupt industries by unleashing the power of software and data.
Redis
Redis is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine.
RabbitMQ
RabbitMQ is a message-queueing software also known as a message broker or queue manager.
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.
Webpack
Webpack is a module bundler. Webpack can take care of bundling alongside a separate task runner.
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"
}
]
}
- Run the following command
cd erxes/cli
yarn install
- Then run the following command to start erxes with your newly installed plugin
./bin/erxes.js dev