Add-on code requirements
What your add-on code must and must not do to pass review and keep working across eLabNext updates.
Add-on code requirements
Every add-on submitted to the Marketplace is reviewed against the rules on this page. Check your code against them before you submit. The rules exist for three reasons: your add-on keeps working when eLabNext changes, it cannot break other add-ons or the page it runs on, and it cannot leak the data of the people who install it.
This page covers the code. The documentation, support, and listing requirements for a public add-on are on Public add-on requirements.
Structure your file
An add-on is one JavaScript file. You can author it in TypeScript or with a bundler, but the uploaded file is the compiled .js file.
The file declares one global variable, the rootVar, and wraps everything else in an immediately invoked function that receives it. The rootVar is the add-on identifier you chose when you created the add-on in the Developer Platform, and the starter template you download there already declares it:
var MY_ADDON = {};
(function (context) {
function renderBadge(sample) {
// helpers live here, next to init, so every handler can reach them
}
context.init = function (configuration) {
eLabSDK2.Inventory.Sample.SampleDetail.registerAction({
id: 'MY_ADDON_show_badge',
label: 'Show badge',
icon: 'fas fa-tag',
onClick: function () {
renderBadge(eLabSDK2.Inventory.Sample.SampleDetail.getSample());
},
});
};
})(MY_ADDON);- Do declare exactly the identifier the Developer Platform shows on the Code page. The upload is refused when the code does not declare it, and eLabNext cannot load an add-on whose rootVar does not match.
- Do define helper functions inside the wrapper but outside
init, so click handlers and laterinitruns can call them. - Do put real work in
init. An emptyinitmeans the add-on does nothing, and the review treats it as unfinished. - Don't declare anything else at the top level. No second global, no
window.myHelper = .... Other add-ons and eLabNext itself share that scope. - Don't replace built-ins such as
window.fetchorJSON.parse, and don't add methods toObject.prototype,Array.prototype, or any other native prototype. That changes behavior for every script on the page. - Don't write to
__proto__orconstructor, and don't deep-merge objects that came from outside your add-on without filtering those keys. Use aMaporObject.create(null)when the keys come from data.
Keep the file focused. A few hundred lines is normal. A bundle approaching a megabyte is examined for what it contains.
Know when your code runs
eLabNext calls init once the document is parsed and the SDK is ready. On the inventory pages, which navigate without reloading, it calls init again on every route change, so make init safe to run more than once. Registering an action or section with an id that already exists replaces the earlier registration; elements you create yourself are not de-duplicated for you.
- Don't wait for
DOMContentLoaded, poll withsetTimeoutorsetInterval, or guard calls withif (window.eLabSDK2). The document is parsed and the SDK is loaded beforeinitis called. - Don't read the first argument of
initas page data. It is your add-on's configuration (see Configure your add-on). Read page data through the SDK.
What you do inside init depends on what the add-on is for:
| Your add-on | In init |
|---|---|
| Adds a button, bulk action, or section to a page | Call the SDK's register... or add... method directly. The SDK shows it when the page is there and runs your handler on click. |
| Needs page data as soon as a specific page opens | Use that page's ready callback, for example eLabSDK2.Inventory.Sample.SampleDetail.onSampleDetailReady(callback, 'MY_ADDON_ready'). Fall back to eLabSDK2.onAfterPageLoad(callback, 'MY_ADDON_ready') only when no page-specific callback exists. |
| Shows a floating panel or timer on every page | Create it right away. Ready callbacks may never fire for a page that has no such event. |
// Wrong: runs at load time, probably before the user opens a sample
context.init = function () {
var sample = eLabSDK2.Inventory.Sample.SampleDetail.getSample();
renderBadge(sample);
};
// Right: runs when the sample detail page is ready
context.init = function () {
eLabSDK2.Inventory.Sample.SampleDetail.onSampleDetailReady(function () {
renderBadge(eLabSDK2.Inventory.Sample.SampleDetail.getSample());
}, 'MY_ADDON_ready');
};Timers are allowed when timing is the feature, such as a countdown or a clock. They are not allowed as a way to wait for the page or for data.
Read and write eLabNext data
- Do use only functions that exist in the SDK reference and endpoints that exist in the API reference. A function or endpoint that is not in the documentation is not supported.
- Do prefer SDK2 when both SDKs offer the same function. An add-on can use SDK1 and SDK2 together.
- Do
awaitthe SDK calls that return a Promise, such asreloadSample, and call the ones that don't, such asgetSample, as plain functions. The reference shows the return type of each.
async function refreshQuantity(sampleID) {
await eLabSDK2.Inventory.Sample.SampleDetail.reloadSample(sampleID);
var sample = eLabSDK2.Inventory.Sample.SampleDetail.getSample();
eLabSDK2.UI.Toast.showToast('Reloaded ' + sample.name, 5000);
}Calling the REST API from an add-on
eLabSDK.API.call sends the request as the logged-in user, so you never handle tokens or base URLs. The full guide is API usage in an add-on; the rules that matter for review are:
- Do give
patha relative path such assamples/123/meta. The SDK prepends the API base URL of the user's environment, so a full URL produces a broken request. - Do wrap the call in a Promise before you
awaitit. The function is callback-based; awaiting it directly gives you the request object, not the response data. - Don't call the eLabNext API with
fetch,XMLHttpRequest, or a library such as axios, and don't build anAuthorizationheader yourself. That ties your add-on to today's session mechanics and breaks when they change. - Don't use
eLabSDK.API.callto read data the SDK already gives you. Read through SDK getters, write through the API.
function fetchSampleLogs(sampleID) {
return new Promise(function (resolve, reject) {
eLabSDK.API.call({
method: 'GET',
path: 'samples/' + sampleID + '/logs',
queryParams: { $records: 100 },
onSuccess: function (xhr, status, response) {
resolve(response);
},
onError: function (xhr, status, response) {
reject(new Error('Could not load sample logs'));
},
});
});
}The body of a PUT or POST to samples/{id}/meta uses key, value, and sampleDataType, not sampleMetaKey and sampleMetaValue. The type of a number field is NUMERIC; INTEGER is not a valid type.
Keep API usage proportionate to the task. Paging through an entire inventory to find one sample is replaced with a filtered query on review.
Work with the page
The page belongs to eLabNext. Element IDs, class names, and structure change between releases, and the SDK is what keeps your add-on working through those changes.
- Do add buttons, sections, tabs, dialogs, and toasts through the SDK:
registerAction,addSection,addTab,eLabSDK2.UI.Dialog,eLabSDK2.UI.Toast. - Don't read or change elements eLabNext rendered.
document.getElementById('sampleHeader').innerHTML = ...breaks on the next release and is rejected on review.
Elements you create yourself
You may create your own elements, for example a floating timer, and append them to the page. When you do:
- Do prefix every ID, class name, and style element with your rootVar:
MY_ADDON_panel,.MY_ADDON_button,MY_ADDON_styles. Generic names collide with other add-ons. - Do check that the element does not already exist before you create it.
initruns again on navigation, and without the check you get duplicates. - Do set
position: fixedand the coordinates before you append a floating element, and make suredocument.bodyexists. - Do check an element exists before you read its properties. A missing element throws and stops your add-on.
function ensureStyles() {
if (document.getElementById('MY_ADDON_styles')) {
return;
}
var style = document.createElement('style');
style.id = 'MY_ADDON_styles';
style.textContent = '.MY_ADDON_panel { position: fixed; top: 20px; right: 20px; }';
document.head.appendChild(style);
}Binding events in dialogs and sections
Dialog and section content is an HTML string that eLabNext renders for you. The elements inside it exist only after rendering, so attach your event listeners in the onRendered callback, never after a setTimeout:
eLabSDK2.UI.Dialog.showDialog({
id: 'MY_ADDON_dialog',
title: 'Enter a barcode',
content: '<input id="MY_ADDON_barcode" type="text" />',
onRendered: function () {
var input = document.getElementById('MY_ADDON_barcode');
if (input) {
input.addEventListener('change', function () {
lookUp(input.value);
});
}
},
});addSection on the inventory detail pages accepts the same onRendered callback.
Choosing the right surface
| You want | Use |
|---|---|
| Input from the user, a confirmation, or an error that blocks the page | eLabSDK2.UI.Dialog.showDialog, showConfirmDialog, showErrorDialog |
| A short status message | eLabSDK2.UI.Toast.showToast |
| Content that belongs to a sample, storage unit, or experiment | addSection or addTab on that page's SDK class; link it to related content with position where supported |
| A panel that is always visible, independent of the page | Your own fixed-position element |
Use libraries
- Do use open-source libraries with a permissive license (MIT, Apache, BSD). Commercial or proprietary libraries are not accepted.
- Don't use jQuery or MooTools, and don't write MooTools-style
new Class({...})definitions. Both are being phased out of the platform, and the older examples that used them are outdated. - Don't paste a library's source into your file. Load it from a CDN, or bundle it with a build tool.
A bundle produced by a build tool, framework runtime included, is accepted. Utility libraries such as a Markdown parser or a date library are better loaded from a CDN, which keeps your file small and lets add-ons share one copy.
When you load from a CDN:
function loadMarked() {
return new Promise(function (resolve, reject) {
if (typeof marked !== 'undefined') {
resolve();
return;
}
var script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js';
script.integrity = 'sha384-...';
script.crossOrigin = 'anonymous';
script.onload = resolve;
script.onerror = function () {
reject(new Error('Could not load marked'));
};
document.head.appendChild(script);
});
}- Do check whether the library is already present. Another add-on may have loaded it.
- Do pin the version in the URL and set
integrityandcrossOrigin, so a changed or compromised file fails to load instead of running. - Do handle
onerror. A library that fails to load should produce a clear error, not a crash somewhere later. - Don't compute the script URL at runtime. A
srcbuilt from configuration or user input is the same risk aseval.
Keep it secure
Your add-on runs with the permissions of whoever installed it. A violation of any of these rules is a rejection.
- Don't put API keys, tokens, passwords, or other secrets in the source. The file is readable by every user of the add-on. Use add-on configuration with a
passwordfield, or OAuth. - Don't use
eval,new Function,document.write, or the string forms ofsetTimeoutandsetInterval. They turn data into code. - Don't put data from the API, a form, the page, or your configuration into
innerHTMLor into dialog and section content without sanitizing it. SettextContent, build the elements withcreateElement, or run the string through a sanitizer such as DOMPurify. - Don't read
document.cookie,localStorage, orsessionStorageand send the value anywhere. Session data stays in the browser. - Don't listen for
messageevents without checkingevent.origin, and don'tpostMessagewith'*'as the target origin.
// Wrong: a sample name that contains <script> runs on the page
cell.innerHTML = '<b>' + sample.name + '</b>';
// Right: the browser escapes it
var bold = document.createElement('b');
bold.textContent = sample.name;
cell.appendChild(bold);Calling your own or a third-party service is allowed. Keep the credential in add-on configuration, send it in a header rather than in the URL, and expect the reviewer to ask where it is stored and how a user can revoke it.
Handle errors and give feedback
- Do give every API and SDK call an error path, and do something in it. At minimum
console.error(error); for anything the user triggered, also show a toast or an error dialog. An emptycatchor an emptyonErrorhides the failure from the user and from you. - Don't use
alert(). UseeLabSDK2.UI.Dialog.showErrorDialogoreLabSDK2.UI.Toast.showToast. - Don't navigate with
window.location.href.window.location.reload()after a data change is fine. - Do show a readable property of an object, such as
nameorlabel.[object Object]in the interface is a bug. - Do give every registered action a Font Awesome icon, such as
'fas fa-tag', that matches what it does.
try {
await checkOut(sampleIDs);
eLabSDK2.UI.Toast.showToast('Checked out ' + sampleIDs.length + ' samples', 5000);
} catch (error) {
console.error(error);
eLabSDK2.UI.Dialog.showErrorDialog('Check-out failed', error.message);
}Configure your add-on
User-editable settings go through the configuration schema, not through hardcoded values or your own storage. The schema and default values are uploaded separately in the Developer Platform and arrive merged as the first argument of init. Name that parameter configuration and read it directly. The platform has already merged the user's values with the defaults, so don't merge them again. Side-loading passes no configuration, and Add-on configuration shows the fallback for that.
Before you submit
- One
.jsfile, the rootVar from the Developer Platform, everything else inside the wrapper,initdoes real work. - No other globals, no changes to built-ins or native prototypes.
- No
DOMContentLoaded, nosetTimeoutto wait, noif (window.eLabSDK2)guards. - Page data comes from SDK getters, not from the
initargument. - Every SDK or API function you call exists in the reference.
eLabSDK.API.calluses relative paths, a Promise wrapper, and anonErrorthat does something.- No
fetchto the eLabNext API, no handmadeAuthorizationheader. - Sample metadata uses
key,value,sampleDataType, andNUMERIC. - No reads or writes to elements eLabNext rendered.
- Your own IDs, classes, and styles are prefixed with the rootVar and created only once.
- Dialog and section events are bound in
onRendered. - Libraries are open-source, pinned with
integritywhen loaded from a CDN, and never jQuery or MooTools. - No secrets in the source, no
eval, no unsanitizedinnerHTML, no cookie or storage reads that leave the browser. - Errors are logged and shown; no
alert(), nowindow.location.href. - Every action has an icon.
- Settings come from the configuration schema.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| The add-on never does anything | init ran before the user reached the page. Use a ready callback or a register... method, see Know when your code runs. |
await eLabSDK.API.call(...) returns an object with readyState | You awaited the call directly. Wrap it in a Promise, see Calling the REST API from an add-on. |
| Panels or styles appear twice after navigating | init ran again. Check for an existing element before creating it. |
| A metadata update returns 200 but nothing changes | The body used sampleMetaKey/sampleMetaValue or the type INTEGER. Use key, value, sampleDataType, and NUMERIC. |
A library is undefined when your code runs | The script tag was appended but not awaited. Resolve a Promise in onload and wait for it before you use the library. |
Updated about 6 hours ago