eLabBlog

Developing Custom Solutions With eLabNext Add-ons

By Tigran Sargsyan 7 min read 02 Apr 2024

AOByte recently started a new partnership with eLabNext, an all-in-one lab management software. eLabNext solutions help to improve the quality of the research by providing all-around tools for any lab. Due to its rapid expansion, eLabNext has decided to release an SDK, allowing developers to create new add-ons that other users can install on the eLabNext dashboard. Add-ons allow users to integrate 3rd party software into dashboards, software such as Dropbox, Google Drive, etc. Add-ons also enable users to add functionality to their dashboard without waiting for their desired functionality to be released by eLabNext.

Our company is proud to accompany eLabNext on its journey. Seeing a growing demand, we’ve decided to share part of our journey of creating custom add-ons. This article is a good place to start if you’re interested in eLabNext add-on development.

Starting Add-on Development

To start add-on development, you must first turn on Developer mode from settings. Navigate to Account Settings > Developer. Developer mode is turned on by simply toggling the switch. In turned-on Developer mode, the SDK will attempt to inject an add-on JavaScript file from the “Add-on script URL” on the page load. A single JavaScript file will be loaded at runtime on page load each time you browse the eLabNext dashboard.

Now, let’s try to create a simple add-on. Before jumping into coding, here are two valuable resources: eLabNext SDK documentation and eLabNext REST API documentation.

Use the Download Template from the Developer settings page to create an empty add-on. This is a working sample add-on, which can be fed to the SDK via an HTTP server of your choice. Our team is using a NodeJS-based http-server for development purposes. The add-on below achieves a simple task of displaying the tasks table in the dashboard. It also allows users to create and delete tasks.


/*
@rootVar: SAMPLE_ADDON
@name: Sample
@description: Sample addon
@author: Stepan Smbatyan
@version: 1.0.0
*/
var SAMPLE_ADDON = {};

((context) => {
  context.init = (config) => {
    $(() => {
      context.SampleAddon = new context.SampleAddon(config);
    });
  };

  context.SampleAddon = new Class({
    Implements: [Options, Events],
    Extends: eLabSDK.Base,
    options: {},
    initialize: function (config) {
      // Store a reference to the function's context
      var self = this;
      // Set the options for the application using the provided configuration
      self.setOptions(config);

      $(document).ready(() => {
        const currentPage = Helper.History.get('pageID');

        const pageID = currentPage || new URLSearchParams(window.location.search).get('pageID');

        renderTaskPage();

        if (pageID === 'tasks') {
          getTasks().then(({ data }) => {
            renderTaskTable(data);

            addDeleteBtnListener();
          });
        }
      });
    },
  });

  // #TODO: remove context.init() when upload as add-on to marketplace
  context.init();
})(SAMPLE_ADDON);

// ======================================= DOM =======================================

/**
 * Renders the task list UI by updating the browser history, creating a button and table,
 * filling the table with task data, and updating the main content section with the table container.
 * @param {Event} e - Optional event object. If provided, prevents the default action.
 */
const renderTaskTable = (data) => {
  const button = createAddTaskButton();
  $('#main-content')
  .html('<section id="tableContainer"></section>')
  .prepend(button.render());

  const table = createTaskTable();
  table.data = data;
  table._renderHTML();
};

/**
 * Creates a custom page for tasks using eLabSDK.
 * This function initializes a new CustomPage object with specified configurations.
 * @returns {CustomPage} A CustomPage object representing the task page.
 */
const renderTaskPage = () => {
  return new eLabSDK.CustomPage({
    rootVar: '.nav-main-level',
    pageID: 'tasks',
    mainMenu: 'Tasks',
    subMenu: 'Task list',
  });
};

/**
 * Creates a button element using the eLabSDK.GUI.Button constructor.
 * The button is configured with a label, CSS class,
 * and an action to show a dialog for updating tasks.
 * @returns {eLabSDK.GUI.Button} - A button element configured to add a new task when clicked.
 */
const createAddTaskButton = () => {
  return new eLabSDK.GUI.Button({
    label: 'Add New Task',
    class: 'addNewTaskBtn',
    action: () => showDialog(DIALOG_CONFIGS.CREATE, createTaskAction),
  });
};

const addDeleteBtnListener = () => {
  $('.deleteBtn').on('click', (e) => {
    const id = e.currentTarget.getAttribute('_dataId');

    showDialog(DIALOG_CONFIGS.DELETE, () => deleteTaskAction(id));
  });
};

/**
 * Creates a table element using the Helper.Table.create method.
 * The table is configured with specified target container, data
 * and columns for displaying task information.
 * @returns {HTMLElement} - A table element configured to display task information.
 */
const createTaskTable = () => {
  return Helper.Table.create({
    target: 'tableContainer',
    caption: null,
    data: {},
    columns: [
      {
        name: 'Full Name',
        key: 'fullName',
        width: '20%',
        cellRender: ({ creator }) => `<b>${creator.fullName}</b>`,
      },
      {
        name: 'Title',
        key: 'title',
        width: '20%',
        cellRender: ({ title }) => `<span>${title || '-'}</span>`,
      },
      {
        name: 'Description',
        key: 'contents',
        width: '45%',
        cellRender: ({ contents }) => `<span>${contents || '-'}</span>`,
      },
      {
        name: 'Created',
        key: 'created',
        width: '10%',
        cellRender: ({ created }) => `<span>${created.split('T')[0]}</span>`,
      },
      {
        name: 'Action',
        key: 'actions',
        width: '5%',
        cellRender: ({ taskID }) => `
          <p class='deleteTranslationIcon deleteBtn' _dataId="${taskID}">
            <i class='fa fa-trash-alt _actionIcon' title='Delete translation'></i>
          </p>
        `,
      },
    ],
  });
};

// ======================================= MODAL =======================================

/**
 * Initiates the deletion of a task identified by its taskId asynchronously.
 * Upon successful deletion, closes any open dialogs, reloads the page to reflect the changes.
 * @param {string} taskId - The ID of the task to be deleted.
 * @returns {Promise<void>} - A Promise that resolves after the task deletion and page reload.
 */
const deleteTaskAction = async (taskId) => {
  await deleteTask(taskId);
  Dialog.closeWait();
  window.location.reload();
};

/**
 * Adding a new task with the provided title and description,
 * closing the dialog window, and reloading the current page.
 * @returns {Promise<void>} A promise that resolves once the actions are updated.
 */
const createTaskAction = async () => {
  const title = $('#title').val();
  const description = $('#description').val();

  await addTask({ title, description });
  Dialog.closeWait();
  window.location.reload();
};

/**
 * Displays a dialog window with specified configuration options and a custom button, 
 * calling the provided callback function when the custom button is clicked.
 *
 * @param {Object} config - The configuration object for the dialog window.
 * @param {string} config.title - The title of the dialog window.
 * @param {number} config.width - The width of the dialog window.
 * @param {string} config.btnOk - The label for the OK button.
 * @param {string} config.btnCancelLabel - The label for the Cancel button.
 * @param {string} config.content - The content to be displayed in the dialog window.
 * @param {string} config.customButtonLabel - The label for the custom button.
 * @param {string} config.customButtonStyle - The style for the custom button.
 * @param {Function} callback - The callback function to be called when the custom button is clicked.
 * @returns {void}
 */
const showDialog = (config, callback) => {
  const {
    title,
    width,
    btnOk,
    btnCancelLabel,
    content,
    customButtonLabel,
    customButtonStyle,
  } = config;

  Dialog.show({
    title,
    width,
    btnOk,
    btnCancelLabel,
    content,
    customButtons: [
      {
        label: customButtonLabel,
        style: customButtonStyle,
        fn: callback,
      },
    ],
  });
};

// ======================================= CONSTANTS =======================================

const DIALOG_CONFIGS = {
  DELETE: {
    title: 'Delete Task',
    width: '550',
    btnOk: false,
    btnCancelLabel: 'Close',
    content: '<p>Are you sure you want to delete this task?</p>',
    customButtonLabel: 'Delete Task',
    customButtonStyle: 'background:#fe810',
  },
  CREATE: {
    title: 'Add New Task',
    width: '550',
    btnOk: false,
    btnCancelLabel: 'Close',
    content: `
      <section>
        <input id="title"  type="text" placeholder="Title" />
        <textarea id="description" placeholder="Description" style="padding-top: 8px;"/>
      </section>
    `,
    customButtonLabel: 'Add Task',
    customButtonStyle: 'background:#fe810',
  },
};


// ======================================= API =======================================

/**
 * Retrieves tasks by making a GET request to eLabSDK.
 *
 * @returns {Promise<Array>} A promise that resolves with an array of tasks upon successful retrieval, or rejects with an error response.
 */
const getTasks = () => new Promise((resolve, reject) => {
  new eLabSDK.API.Call({
    method: 'GET',
    path: 'tasks',
    onSuccess: (xhr, status, response) => {
      resolve(response);
    },
    onError: (xhr, status, response) => {
      reject(response);
    },
  }).execute();
});

/**
 * Adds a new task with the provided title and description by making a POST request to eLabSDK.
 *
 * @param {Object} task - An object containing the title and description of the task.
 * @param {string} task.title - The title of the task.
 * @param {string} task.description - The description of the task.
 * @returns {Promise<Object>} A promise that resolves with an array of tasks upon successful retrieval, or rejects with an error response.
 */
const addTask = ({ title, description }) => new Promise((resolve, reject) => {
  const data = {
    assigneeID: 0,
    title,
    contents: description,
  };

  new eLabSDK.API.Call({
    method: 'POST',
    path: 'tasks',
    pathParams: {},
    onSuccess: (xhr, status, response) => {
      resolve(response);
    },
    onError: (xhr, status, response) => {
      reject(response);
    },
  }).execute(data);
});

/**
 * Deletes a task with the specified ID by making a DELETE request to eLabSDK.
 *
 * @param {string} id - The ID of the task to be deleted.
 * @returns {Promise<Object>} A promise that resolves with an array of tasks upon successful retrieval, or rejects with an error response.
 */
const deleteTask = (id) => new Promise((resolve, reject) => {
  new eLabSDK.API.Call({
    method: 'DELETE',
    path: `tasks/${id}`,
    onSuccess: (xhr, status, response) => {
      resolve(response);
    },
    onError: (xhr, status, response) => {
      reject(response);
    },
  }).execute();
});

One of the crucial things to remember while creating an add-on is prioritizing using SDK and API methods over custom code. A good example will be rendering buttons or making HTTP requests. By using the methods provided by SDK, you can be assured, e.g. buttons will have correct styling, or all the necessary headers will be appended to your HTTP request.

More Complex Add-on Development

Obviously, most of the add-ons that will be created will be more complicated than this example. Naturally, while delivering more complex features, developers would like to use the power of breaking code into modules, minimizing code for production, writing test cases for their code, and using all the other advantages of modern web development. While working on the add-ons, we’ve created a boilerplate add-on, allowing users to achieve a project structure, packaging, testing, etc. The project can be found on GitHub.

Remember that the eLabNext SDK is gaining momentum; thus, the documentation needs to be completed. Please contact our team if you find yourself in a situation where help might be required. Our team will continue writing about the eLabNext add-on development process. We will cover topics like submissions of add-ons to eLab Marketplace, tips and tricks of eLabNext add-on development, talking about more complicated features development, and so on.

Recommended For you

2 min read 25 Apr 2024
By Wouter de Jong

Expand Laboratory Digitalisation through Digital Transformation with eLabNext Developer 

Improve the efficiency of your laboratory environment through digitalisation with eLabNext digital lab solutions. 

Read more
9 min read 18 Apr 2024
By Zareh Zurabyan

Biotech Software: To Build or Not to Build, That is the Question…

Explore the evolution of biotech software trends and uncover the advantages of subscribing to biotech SaaS platforms.

Read more
4 min read 11 Apr 2024
By Zareh Zurabyan

Managing and Mapping the Data: Hurdles, Impacts & Solutions

Discover the hurdles, impacts, and solutions in managing diverse data types in life science and biotech laboratories.

Read more

Start your journey to an
All Digital Lab today!

Schedule a Personal Demo for friendly expert guidance and a free lab workflow assessment.

en_GBENG-UK