Lucid Extension API
The Lucid Extension API allows developers to build Extension Packages (sometimes shortened to package) which can be deployed inside Lucid's core products, like Lucidchart and Lucidspark.
An extension package can include any combination of:
- Shape libraries that define custom shapes, including rich, data and formula driven shapes.
- Editor extensions that run custom code directly inside Lucid's editors.
- OAuth providers that configure access to external data.
- Data connectors that connect Lucid documents to external data.
Every extension package includes a manifest file which specifies the ID of the package, and declares any shape libraries, editor extensions, oauth providers or data connectors which are included in the package.
The Extension API includes two npm modules to help you create your extensions:
- lucid-package which can be used to create, bundle, and test extension packages.
- lucid-extension-sdk which can be used by editor extensions to extend Lucid's core products.
You can see ongoing release notes for these packages here.
You can find example extension packages in Lucid's repository of Sample Lucid Extensions.
Getting started
Unlocking developer features
To test an app built on the Extension API, you'll need access to the Developer Menu. To distribute your app, you'll need access to the Developer Portal. Instructions for unlocking these two tools can be found here.
Create a new package
An extension package is a set of code, content, and configuration for Lucid products that is installable by individual Lucid users or by a Lucid account admin for use by their entire account. The
To get started, run the following commands in a directory that you want to contain your extension packages:
$ npm install lucid-package
$ npx lucid-package create my-new-package-name
Add an editor extension
An editor extension is a piece of custom code that executes inside a Lucid product such as Lucidchart or Lucidspark. The
To add a new editor extension to your package, run the following commands:
$ cd my-new-package-name
$ npx lucid-package create-editor-extension my-extension-name
Debug your editor extension
You don't need to bundle, upload, and install your extension in order to run it and make sure it works. The following command will start
To start the debug server, run the following command:
$ npx lucid-package test-editor-extension my-extension-name
You can then enable loading of your local code in the Developer menu in Lucidchart by clicking "Load local extension". The page will refresh and your editor extension code will run.
The main entry point to your new editor extension is in
For all published editor extensions, and by default for this debug server as well, your code runs in a sandboxed JavaScript VM for security. However, this makes debugging difficult. If you turn on the "Debug local extensions (no sandbox)" option in the Developer menu, your code will be run via a scoped eval, allowing you to use the standard browser debugging tools to examine and step through your code.
We recommend that you do all final validation of your editor extension with the normal sandbox enabled, however, as you may have inadvertently used features not allowed in the sandbox that won't work once you release your extension. For example, editor extensions are not allowed to directly access any browser DOM APIs.
Debugging multiple editor extensions together
You can watch and serve multiple editor extensions in the same package at the same time.
To start the debug server for multiple editor extensions, run the following command:
$ npx lucid-package test-editor-extension my-extension-name my-other-extension-name my-third-extension-name
Bundle your package for upload
First, you should create your package on the Lucid Developer Portal. Clicking on your extension should take you to a page with a URL in the form
Then, once your editor extension (and other extension package content) works the way you want, you can bundle it for upload to the Lucid Developer Portal with the following command:
$ npx lucid-package bundle
This will create the file
Core concepts
Extension packages
An extension package defines an extension to Lucid's products. It includes all the frontend code for any editor extensions in the package, any shape libraries your extension provides, and specifies all data connectors and OAuth providers your extension will leverage.
Packages are uploaded and managed in the developer dashboard. You can learn more about how extension packages are structured here.
Editor extensions
An editor extension is custom JavaScript code you write that gets executed in a Lucid product. Editor extensions use the lucid-extension-sdk npm package to interact with the UI of the application and the content currently loaded in a Lucid document.
Editor extensions can add new menu items to Lucid's menus, new blocks with unique functionality to the canvas, and new interactions to existing elements in Lucid documents. Editor extensions can also leverage OAuth APIs and data connectors to bring external data into Lucid documents. You can learn more about how to create editor extensions here.
Shape libraries
A shape library is a collection of custom shapes you create that can be used in Lucid documents. Your shapes can leverage data, conditional formatting, formulas, and much more to create intelligent, interactive experiences. Learn more about custom shapes and how to create them here. Learn more about adding custom shape libraries to your extension here.
Data connectors
A data connector is an API you create that editor extensions in your package can use to fetch and manage external data. In its simplest form, a data connector could just be a single endpoint that fetches and returns some data from an external API. For more complicated integrations, data connectors can post data back to documents asynchronously. Data connectors can also implement specific endpoints for things like:
- Propogating changes to data made in Lucid's editors back to the data source.
- Automatically fetching changes to data made in data sources so Lucid documents have the most recent version.
- Setting up and managing webhook relationships with data sources to track data changes made ouside of Lucid.
A data connector must provide a public URL that Lucid's servers can make requests to. Learn more about building data connectors here.
Extension packages
Extension packages can be created automatically using the lucid-package CLI tool:
$ npx lucid-package create my-package
Extension package structure
An extension package is structured like this:
> my-package
> editorextensions
└── ...
> shapelibraries
└── ...
> dataconnectors
└── ...
└── .gitignore
└── manifest.json
The package has two settings files:
- A
.gitignore file for managing version control. - A manifest file called
manifest.json for specifying package configuration.
Packages also contain two folders:
- An
editorextensions folder which will house all the code for the package's editor extensions. - A
shapelibraries folder which will house definitions for shape libraries added by the package.
Manifest file
Every extension package has one manifest file called
{
"id": "9347ghfne-932nfi92hnk-sj3i8ns-0qk34e98",
"version": "1.0.0",
"extensions": [],
"shapeLibraries": [],
"oauthProviders": [],
"dataConnectors": []
}
A manifest file contains the following fields:
-
id : the globaly unique identifier for your extension package. When you create an application in the developer portal (see Bundle your package for upload) you will be given an ID to use for this value. -
version : the current version of your extension package. When uploading your package to the developer portal, its package version must be higher than the most recently uploaded version. Running the bundle command will increase the version automatically. -
extensions : declarations and settings for editor extensions in your package. -
shapeLibraries : declaration and settings for shape libraries in your package. -
oauthProviders : configurations for any OAuth providers your package will leverage.
Editor extensions
Editor extensions can be created automatically using the lucid-package CLI tool:
/my-package$ npx lucid-package create-editor-extension my-editor-extension
Editor extension file structure
Editor extensions are structured like this:
> editorextensions
> my-editor-extension
> node_modules
└── ...
> resources
└── import.html
└── resource.d.ts
> src
└── extension.ts
└── importmodal.ts
└── interop.d.ts
└── package-lock.json
└── package.json
└── tsconfig.json
└── webpack.config.js
The
-
import.html : includes a very basic import modal to get you started with an import flow. -
resource.d.ts : responsible for specifying what type of content you want to include as resources.
The
-
extension.ts : the entry point for your extension code. -
importmodal.ts : example code illustrating the use of modals. -
interop.d.ts : standard method definitions.
The
Editor extension manifest
To add an editor extension to your extension package, you need to declare the editor extension in your
Here is an example of what an editor extension manifest entry might look like:
{
// ...
"extensions": [
{
"name": "my-editor-extension",
"title": "My Editor Extension",
"products": ["chart", "spark"],
"codePath": "editorextensions/my-editor-extension/bin/extension.js",
"scopes": [
"READ",
"WRITE",
"DOWNLOAD",
"SHOW_MODAL",
"CUSTOM_UI",
"NETWORK"
]
}
]
}
An editor extension manifest should have the following fields:
-
name : the name of the extension. This should match the name of the folder that contains the editor extension. -
title : a user facing title string that is used in Lucid components like modals and dropdown menus. -
products : the products that the editor extension can be used in:["chart", "spark", "teamspaces"] . -
codePath : the file path to the entry point of your extension. By default, this will be set tobin/extension.js , which is created when you bundle your package. -
scopes : the scopes that your editor extension has access to. These are the scopes that an editor extension can have:Scope Description READ Allows you to read elements and data from the document WRITE Allows you to write elements and data from the document SHOW_MODAL Allows you to show modals CUSTOM_UI Allows you to create custom panels in the right dock in Lucidchart DOWNLOAD Allows you to enable downloading data as a file NETWORK Allows you to have direct access to a simple XHR API OAUTH_TOKEN Allows you to access the user's OAuth token USER_INFO Allows you to use the UserProxy to access user information (see Users)
Document content
When a user opens a Lucid product, the content they are viewing is a document. The document contains all of the information about what is displayed, as well as any data that may be used in that content.
The extension SDK provides a mechanism for reading and writing the content of the current document through a series of proxy classes. You can read and write to a
import {DocumentProxy, EditorClient, Menu, MenuType} from 'lucid-extension-sdk';
const client = new EditorClient();
const menu = new Menu(client);
const document = new DocumentProxy(client);
const key = 'myKey'
console.log('Loaded random number:', document.properties.get(key))
client.registerAction('generate-number', () => {
const f = Math.random() * 100
console.log('New random number: '+f)
document.properties.set(key, f)
});
menu.addDropdownMenuItem({
label: 'Generate random number',
action: 'generate-number',
});
Pages
A document consists of one or more pages. In Lucidchart, a document may include more than one page, while in Lucidspark, there is always a single page. A page contains any number of items which may be blocks, lines, or groups.
You can access the list of all pages through the
import {DocumentProxy, EditorClient, Viewport} from 'lucid-extension-sdk';
const client = new EditorClient();
const document = new DocumentProxy(client);
const viewport = new Viewport(client);
client.registerAction('listPages', () => {
for (const [pageId, page] of document.pages) {
console.log(
pageId,
page.getTitle(),
viewport.getCurrentPage() === page ? 'active' : ''
);
}
});
Importing pages
One or more pages can be imported from another document or template if the user has permisson to view the document.
This can be done by using the
import {EditorClient} from 'lucid-extension-sdk';
const client = new EditorClient();
const pageNumsToGet = [0, 1]; // Import the first two pages of the specified document
const documentId = '<documentId>'
await client.importPage(documentId, pageNumsToGet);
To get the document ID, open the document in Lucid and get the ID from the URL (ex. https://lucid.app/lucidchart/{{DOCUMENT_ID}}/edit).
Items
The content on a page consists of blocks, lines, and groups, which are all kinds of items.
All items (and in fact other elements such as pages or the document itself), have a set of properties that can be read and written using their
function dumpProperties(page:PageProxy) {
for(const [blockId, block] of page.allBlocks) {
console.log('Block of class ' + block.getClassName() + ' (' + blockId + '):')
for(const [propertyName, propertyValue] of block.properties) {
console.log(propertyName, propertyValue);
}
}
}
While this allows you access to all the underlying properties of an item, it is not typesafe and should only be used when more specific methods are not available. For example, a
Items may also have data fields and/or whole data records associated with them. You can read more about associating data with items here.
Blocks
A block is a single shape on a page. Different types of blocks are specified by their class name.
Creating a block
Not all block classes have their code loaded on all documents, so before adding a block to a page, you must make sure that the block class has been loaded.
Once you have loaded the block class, you can add it to the page like this:
async function createProcessBlock(page:PageProxy, x:number, y:number) {
await client.loadBlockClasses(['ProcessBlock']);
const block = page.addBlock({
className:'ProcessBlock',
boundingBox:{
x, y, w:200, h:160
}
});
block.textAreas.set('Text', 'The new shape');
}
You only need to load a block class once, so you may also structure your code this way to avoid needing to have
const client = new EditorClient();
function createProcessBlock(page:PageProxy, x:number, y:number) {
const block = page.addBlock({
className:'ProcessBlock',
boundingBox:{
x, y, w:200, h:160
}
});
block.textAreas.set('Text', 'The new shape');
}
async function init() {
await client.loadBlockClasses(['ProcessBlock']);
const menu = new Menu(client);
menu.addDropdownMenuItem({...});
}
init();
By avoiding adding any UI (like menu items, custom panels, etc.) until your needed block classes are loaded, you can then operate synchronously in the rest of your code.
Class specific functionality
While all properties of all blocks are available through
When you get a
ERD block
ERD blocks are returned as a
function dumpERD(page: PageProxy) {
for (const [blockId, block] of page.allBlocks) {
if (block instanceof ERDBlockProxy) {
console.log('ERD block: ' + block.getName());
for(const field of block.getFields()) {
console.log('Field: ' + field.getName() + ': ' + field.getType());
}
}
}
}
Generic Lucid cards
Lucid cards blocks are returned as a
async function createGenericLucidCardBlock(page: PageProxy, title: string, description: string) {
await client.loadBlockClasses(['LucidCardBlock']);
const block = page.addBlock({
className:'LucidCardBlock',
boundingBox:{
x, y, w:200, h:160
}
});
if (block instanceof CardBlockProxy) {
block.setTitle(title);
block.setDescription(description);
}
}
Identifying custom shapes
If you use custom shapes from a shape library in your extension, those blocks will be instances of
function findInstancesOfMyShape(page: PageProxy) {
for (const [blockId, block] of page.allBlocks) {
if (block instanceof CustomBlockProxy) {
if (block.isFromStencil('my-library', 'my-shape')) {
console.log('Found custom shape "my-shape": ' + block.id);
}
}
}
}
You can also extend
export class MyCustomBlock extends CustomBlockProxy {
public static library = 'my-shape-library';
public static shape = 'my-custom-shape';
public getTextContent() {
const taName = this.getStencilTextAreaName('TextContent');
if (!taName) {
return '';
}
return this.textAreas.get(taName);
}
public setTextContent(text: string) {
const taName = this.getStencilTextAreaName('TextContent');
if (!taName) {
return '';
}
return this.textAreas.set(taName, text);
}
}
CustomBlockProxy.registerCustomBlockClass(MyCustomBlock);
When you call
const client = new EditorClient();
const document = new DocumentProxy(client);
client.registerAction('log-custom-text-content', () => {
for(const [pageId, page] of document.pages) {
for(const [blockId, block] of page.blocks) {
if(block instanceof MyCustomBlock) {
console.log(block.getTextContent());
}
}
}
});
You can also create custom blocks directly from your extension code. To do this, use the
async function createCustomBlock(page: PageProxy, libraryName: string, shapeName: string) {
const customBlockDef = await client.getCustomShapeDefinition(libraryName, shapeName);
if (!customBlockDef) {
return;
}
const customBlock = page.addBlock(customBlockDef);
customBlock.textAreas.set('Text', 'My Custom Shape');
}
Text on blocks
Many classes of blocks have one or more text areas. These text areas can be enumerated, read, and written using
function changeText(page: PageProxy) {
for (const [blockId, block] of page.allBlocks) {
for (const [textAreaKey, plainText] of block.textAreas) {
block.textAreas.set(textAreaKey, plainText + ' (changed)');
}
}
}
You can read or write text styles on text areas using
Changing text styles is asynchronous and must be
import {TextMarkupNames, EditorClient, Menu, MenuType, Viewport} from 'lucid-extension-sdk';
const client = new EditorClient();
const menu = new Menu(client);
const viewport = new Viewport(client);
client.registerAction('toggle-bold', async () => {
for (const item of viewport.getSelectedItems()) {
for (const ta of item.textAreas.keys()) {
const oldStyle = item.textStyles.get(ta);
await item.textStyles.set(ta, {
[TextMarkupNames.Bold]: !oldStyle[TextMarkupNames.Bold],
});
}
}
});
menu.addDropdownMenuItem({
label: 'Toggle bold',
action: 'toggle-bold',
});
Positioning
Blocks (and other items) have a bounding box you can read with
Moving or resizing a block is often subject to constraints. For example, some blocks have a minimum width or height, or do not allow resizing on one axis. Other blocks have side effects when moved, such as moving the other items along with a magnetized container.
For this reason, you cannot directly set the
To properly position a block, it may be necessary to determine the user's focus on the board. To achieve this, you can utilize
function createProcessBlock(page:PageProxy, viewport:Viewport, dx:number, dy:number) {
const {x, y} = viewport.getVisibleRect();
const block = page.addBlock({
className:'ProcessBlock',
boundingBox:{
x + dx, y + dy, w:200, h:160
}
});
block.textAreas.set('Text', 'The new shape');
}
Images
You can easily insert an image using either a public or a data URL:
function createImage(page:PageProxy, x:number, y:number) {
page.addImage({
boundingBox:{
x, y, w:150, h:150
},
fillStyle: {
url: 'https://cdn-cashy-static-assets.lucidchart.com/marketing/images/LucidSoftwareFavicon.png',
position: SimpleImageFillPosition.Fill,
},
});
}
Lines
A line is a connector with two endpoints. Each endpoint may or may not be connected to a block, or to another line.
Creating a line
To create a line, you specify each endpoint as either a free-floating endpoint (just x/y coordinates), a block-connected endpoint, or a line-connected endpoint:
function connectBlocks(block1: BlockProxy, block2: BlockProxy) {
block1.getPage().addLine({
endpoint1: {
connection: block1,
linkX: 0.5,
linkY: 1,
},
endpoint2: {
connection: block2,
linkX: 0.5,
linkY: 0,
},
});
}
Text on lines
Any line can have any number of text areas on it. Each text area consists of its text, a position along the line (from 0 to 1), and a number specifying which side of the line the text should appear on:
function dumpLineText(page: PageProxy) {
for (const [lineId, line] of page.lines) {
for (const [key, text] of line.textAreas) {
const position = line.getTextAreaPosition(key);
if (position) {
if (position.side == 0) {
console.log('Text on line: ' + text);
} else if (position.side == -1) {
console.log('Text to left of line: ' + text);
} else if (position.side == 1) {
console.log('Text to right of line: ' + text);
}
}
}
}
}
You can add or remove text with
As with blocks, you can read and write text style with
Groups
A group is a set of other items, which acts in many ways like a single block.
On
Managing text
Text can exist on both blocks and lines. Reading and writing the text content itself can be done through the textAreas property on either a LineProxy or BlockProxy. For example, the following code reads all the text off the current selection and then replaces it with
import {EditorClient, Menu, MenuType, Viewport} from 'lucid-extension-sdk';
const client = new EditorClient();
const menu = new Menu(client);
const viewport = new Viewport(client);
client.registerAction('hello', async () => {
for(const item of viewport.getSelectedItems()) {
for(const [key, plainText] of item.textAreas) {
console.log('Old value: ' + plainText);
item.textAreas.set(key, 'Hello world');
}
}
});
menu.addDropdownMenuItem({
label: 'Hello World',
action: 'hello',
});
While blocks typically have a set number of text areas on them, lines have any number of text areas, and those text areas can be freely moved around by the user. You can add new text areas, read and write their position on the line, and delete them:
const first = line.addTextArea('Start Left', {location: 0, side: -1});
const second = line.addTextArea('Middle', {location: 0.5, side: 0});
const third = line.addTextArea('End Right', {location: 1, side: 1});
console.log(line.getTextAreaPosition(first));
line.setTextAreaPosition(first, {location: 0.25, side: 0});
line.deleteTextArea(third);
The
The
Hook text editing
Your extension can react to a user editing text in a number of ways:
- It can prevent a user from editing a certain text area.
- It can allow the user to edit a text area, but prevent their edit after the fact (knowing what they typed).
- It can allow the user to edit a text area, but replace the value they typed with another.
To hook text editing, use the Viewport class's hookTextEdit method like this:
import {EditorClient, Viewport} from 'lucid-extension-sdk';
const client = new EditorClient();
const viewport = new Viewport(client);
viewport.hookTextEdit(async (item, textAreaKey, text) => {
if (item.properties.get(textAreaKey) === 'Deny') {
return false;
}
if (item.properties.get(textAreaKey) === 'Numeric') {
return (newValue) => {
if (String(+newValue) === newValue) {
return true;
} else if (isNaN(+newValue)) {
return false;
} else {
return String(+newValue);
}
};
}
return true;
});
This example watches text editing and does the following:
- If the text the user is trying to edit currently says
Deny , then text editing is prevented by returning false from the hook. - If the text the user is trying to edit currently says
Numeric , then the text editing is allowed, but:- if the final value is not a number, the edit is reverted.
- if the final value is not in the default number format, it will be replaced (e.g.
+14.0 ->14 ).
By default, the text editing hook is triggered only at the beginning of text editing. If you wish to listen to changes at a more granular level of key inputs during editing, you may use the
import {EditorClient, Viewport} from 'lucid-extension-sdk';
const client = new EditorClient();
const viewport = new Viewport(client);
const eager = true;
viewport.hookTextEdit(async (item, textAreaKey, text) => {
console.log('Listening for live update from ' + textAreaKey);
}, eager);
In this example, text editing is monitored eagerly, and a message is logged every half a second. It's important to note that you should avoid expensive operations when setting
Both the text editing hook and the callback to be run when editing is complete can be
Menus
One common way for end users to trigger editor extension functionality is through menu items, which can be added either to the main menu or the context menu.
Menu items trigger named actions, which must be registered with EditorClient.registerAction. Named actions may also return values, and can be used as callbacks to determine when a menu item should be visible or enabled.
For example, the following code registers a
const client = new EditorClient();
const menu = new Menu(client);
const viewport = new Viewport(client);
client.registerAction('processBlocksSelected', () => {
const selection = viewport.getSelectedItems();
return (
selection.length > 0 &&
selection.every((item) => item instanceof BlockProxy && item.getClassName() === 'ProcessBlock')
);
});
client.registerAction('makeSelectionRed', () => {
for (const item of viewport.getSelectedItems()) {
item.properties.set('FillColor', '#ff0000ff');
}
});
menu.addContentDockMenuItem({
label: 'Turn red',
action: 'makeSelectionRed',
location: MenuLocation.Edit,
visibleAction: 'processBlocksSelected',
});
menu.addContextMenuItem({
label: 'Turn red',
action: 'makeSelectionRed',
visibleAction: 'processBlocksSelected',
});
Menus items can be added to three places, each has a specialized entry point. You can also use the generic entrypoint addMenuItem.
Dropdown menus
These are the primary dropdown menus (file, edit...) found in the top bar of Lucidchart. In Lucidspark all menus are nested under the one drop down menu in the top left. In Lucidchart a location can be specified (e.g.
Context Menu
This is the right click context menu that exists in both Lucidchart and Lucidspark. In order to avoid clutter in the context menu consider defining visibleAction. See addContextMenuItem for more information.
Lucidspark Left Dock
The content dock only exists in Lucidspark. Like other icons it can be pinned and unpinned. It will default to pinned when first added. An icon link is required. See addContentDockMenuItem for more information.
Modals
While you can build completely custom modals with iFrames, many interactions with the user are simple enough to use pre-built modals provided by the Extension API.
Alert
An alert modal presents plain text to the user with a single button to acknowledge that message. You can configure the alert with a custom title (which defaults to the name of your extension) and/or custom text for the OK button.
The alert method returns a
const client = new EditorClient();
client.alert('This is a message');

Confirm
A confirm modal presents plain text to the user with an OK and Cancel button. You can configure the confirm modal with a custom title (which defaults to the name of your extension) and/or custom text for the OK and Cancel buttons.
The confirm method returns a
const client = new EditorClient();
const menu = new Menu(client);
const viewport = new Viewport(client);
client.registerAction('makeSelectionRed', async () => {
if (await client.confirm('Do you wish to turn the selected blocks red?', undefined, 'Yes', 'No')) {
for (const item of viewport.getSelectedItems()) {
item.properties.set('FillColor', '#ff0000ff');
}
}
});
menu.addDropdownMenuItem({
label: 'Turn red',
action: 'makeSelectionRed',
location: MenuLocation.Edit,
});

Users
By adding the
import {EditorClient, UserProxy} from 'lucid-extension-sdk';
const client = new EditorClient();
const user = new UserProxy(client);
console.log(user.id);
Get user's permission on the document
Knowing the user's permissions on a document can be helpful to determine what actions can be performed by the extension. For example, if the user only has view permissions on a document then the extension will be unable to perform any actions that write content to the document:
import {DocumentProxy, EditorClient, Menu, MenuType, UserProxy} from 'lucid-extension-sdk';
import {DocumentAccessPermission} from 'lucid-extension-sdk/document/documentaccesspermission';
const client = new EditorClient();
const menu = new Menu(client);
const user = new UserProxy(client);
const document = new DocumentProxy(client);
const documentPermission = user.getAccessPermssionOnDocument();
client.registerAction('createBlock', async () => {
const page = document.pages.first();
if (page) {
if (hasWriteAccess()) {
await client.loadBlockClasses(['ProcessBlock']);
page.addBlock({
className: 'ProcessBlock',
boundingBox: {
x: 100,
y: 100,
w: 200,
h: 160,
},
});
} else {
console.log("The user doesn't have write access to the document!");
}
}
});
function hasWriteAccess() {
return documentPermission === DocumentAccessPermission.Edit || documentPermission === DocumentAccessPermission.EditAndShare;
}
menu.addDropdownMenuItem({
label: 'Create Block',
action: 'createBlock',
});
File upload
Menu items can be specified as prompting a file upload. To do this, you first register a named action for receiving the uploaded file content using EditorClient.registerFileUploadAction. This callback will receive an array of files that were selected by the user, including the file names and content as text. Optionally, the content may also be included as a
const client = new EditorClient();
const menu = new Menu(client);
client.registerFileUploadAction('logFileContent', (files: FileUploadData[]) => {
console.log(files);
for (const file of files) {
console.log(file.fileName);
console.log(file.text);
if (file.binary) {
console.log(file.binary);
}
}
});
menu.addDropdownMenuItem({
label: 'Upload file for processing',
file: {
action: 'logFileContent',
accept: 'text/csv',
singleFileOnly: true,
binary: true,
},
});
Links
Your extension can capture pasting URLs from the clipboard to produce a custom block with embedded data. Your extension will register a domain pattern and a callback block that will be called automatically when the user pastes a matching URL onto the canvas.
From the URL you can return an
Created blocks can have one or multiple preview images that a user can browse through.
You can also provide an iframe for a user to view expanded content.
Listen for a URL by calling
import EditorClient from 'lucid-extension-sdk';
const client = new EditorClient();
function performUnfurl(url: string): Promise<UnfurlDetails | undefined> {
const idRegex = /^https:\/\/my\.url\.com\/\w+\/d\/([a-zA-Z0-9-_]+)/;
const id = idRegex.exec(url)?.[1]
if (id) {
try {
return {
providerName: 'myUrlCompany',
providerFaviconUrl: 'my.url.com/logo',
unfurlTitle: 'Block title',
previewImageUrl: 'my.url.com/preview?url=' + url,
};
} catch (error) {
console.log(error);
}
}
return undefined;
}
function performAfterUnfurlCallback(blockProxy: LinkUnfurlBlockProxy, url: string) {
const [fileKey, frameId] = parseLink(url); // for example
if (!fileKey || !frameId) {
return undefined;
}
const nameOfFrame = await getNameOfFrame(fileKey, frameId); // for example
if (nameOfFrame) {
blockProxy.setTitle(nameOfFrame);
}
blockProxy.setIframe({
iframeUrl: 'https://www.my.url.com/embed?embed_host=astra&url=' + url,
aspectRatio: UnfurlIframeAspectRatio.Square,
});
}
client.registerUnfurlHandler('my.url.com', {
unfurlCallback: async (url: string) => performUnfurl(url),
afterUnfurlCallback: async (blockProxy, url) => performAfterUnfurlCallback(blockProxy, url),
});
Domain URL
The domain is required and will be checked against any URLs pasted to the canvas. If the domain matches, your callback will be run. If it doesn't match, then other unfurl handlers will be called so multiple extensions can be running and watching for URLs at the same time. Domains of multiple extensions cannot overlap.
The domain supports subdomains and wildcards in the subdomains. For example:
domain.com
my.domain.com
*.domain.com
We do not support anything after the domain or wildcards in the domain itself. So we DO NOT support
Unfurl callback
unfurlCallback is a required parameter that will be called with the pasted url. Your block should return an UnfurlDetails object to produce a block or undefined to do nothing. This call must run quickly in case any long computations need to be run in the followup async call. The purpose is to get the minimal information to display the new shape.
After unfurl callback
If you have additional, longer work that you need to do for an unfurl you may provide an asynchronous callback named afterUnfurlCallback that will run after the block is generated on the canvas. You will get a LinkUnfurlBlockProxy which will let you update the title, thumbnail and preview URLs, and the details iframe.
Importing links
Links can be imported on to a page as link unfurl blocks.
This can be done by using the
const client = new EditorClient();
const linksToImport = ["https://some.link.com", "https://some.other.link.com"];
new Viewport(this.client).getCurrentPage()?.importLinks(linksToImport);
Links will be unfurled by Lucid based on extensions installed by the user.
iFrames
The Extension API allows you to display custom UI in a sandboxed iframe. The UI displayed in these iframes can communicate with your extension code via asynchronous message passing. You can define custom UI by extending the Modal or Panel class.
Modal example
To display a simple modal, extend the Modal class, passing configuration to the super constructor:
import {EditorClient, Menu, MenuType, Modal} from 'lucid-extension-sdk';
class HelloWorldModal extends Modal {
constructor(client: EditorClient) {
super(client, {
title: 'Hello world',
width: 400,
height: 300,
content: 'Hello from a modal',
});
}
}
const client = new EditorClient();
const menu = new Menu(client);
client.registerAction('hello', () => {
const modal = new HelloWorldModal(client);
modal.show();
});
menu.addDropdownMenuItem({
label: 'Say Hello',
action: 'hello',
});

Panel example
To add a simple panel to the right dock, extend the Panel class, passing configuration to the super constructor. Note that in this case, the
The
import {EditorClient, Panel, PanelLocation} from 'lucid-extension-sdk';
class HelloWorldPanel extends Panel {
constructor(client: EditorClient) {
super(client, {
title: 'Hello world',
iconUrl: 'https://cdn-cashy-static-assets.lucidchart.com/marketing/images/LucidSoftwareFavicon.png',
location: PanelLocation.RightDock,
url: 'hello.html',
});
}
}
const client = new EditorClient();
const panel = new HelloWorldPanel(client);
Persistent panels
Lucid may destroy the panel iframe and recreate it when needed for optimal application performance. Therefore, if you are developing an extension such as a video conference tool where your code needs to run continuously in the background, you can prevent this behavior by using the
class PersistentPanel extends Panel {
constructor(client: EditorClient) {
super(client, {
title: 'Hello world',
content: 'Hello from a panel',
iconUrl: 'https://cdn-cashy-static-assets.lucidchart.com/marketing/images/LucidSoftwareFavicon.png',
location: PanelLocation.RightDock,
persist: true,
});
}
}
Specifying content
Besides just specifying the
If you don't already have a folder named
> my-package
> editorextensions
└── ...
> shapelibraries
└── ...
> dataconnectors
└── ...
> public
└── img
└── ...
└── index.html
└── .gitignore
└── manifest.json
This allows you to reference the HTML file relative to the
import {EditorClient, Modal} from 'lucid-extension-sdk';
export class ImportModal extends Modal {
constructor(client: EditorClient) {
super(client, {
title: 'Import a thing',
width: 600,
height: 400,
url: 'index.html',
});
}
}
To access static resources in the
The iframe is sandboxed, but allows the following:
This means your content may include scripts, external images, and so forth. Each extension is loaded on a unique origin, so the
External content
Although you can, you do not need to bundle the entire UI application into the
If you wish to serve the entire iframe from an external URL, you just need to pass in the external URL as
export class ContentFromElsewhereModal extends Modal {
constructor(client: EditorClient) {
super(client, {
title: 'Loading up something else',
width: 600,
height: 400,
url: 'https://www.example.com',
});
}
}
Specifying content using a content string (Legacy)
The
To package HTML content into your extension as a content string, you can use a TypeScript
declare module '*.html' {
const content: string;
export default content;
}
This indicates that any
{
// ...
"files":[
"resources/resource.d.ts",
],
}
This allows you to import the whole HTML file and use it as the content string like this:
import {EditorClient, Modal} from 'lucid-extension-sdk';
import importHtml from '../resources/import.html';
export class ImportModal extends Modal {
constructor(client: EditorClient) {
super(client, {
title: 'Import a thing',
width: 600,
height: 400,
content: importHtml,
});
}
}
Communicating with iframes
You can communicate with your iframe via message passing. To send a message to your iframe code, call sendMessage on your
window.addEventListener('message', (e) => {
console.log(e.data);
});
To send a message from your iframe code to your
parent.postMessage({
a: 'hello',
b: 'world',
title: 'My Page'
}, '*');
Any message you pass into
protected messageFromFrame(message: JsonSerializable): void {
console.log(message['a']);
console.log(message['b']);
this.page.setTitle(message['title'])
this.hide();
}
Drag and drop blocks from a panel
A common use case for interacting with iframes is dragging and dropping blocks. The following section will go over a very basic implementation using generic javascript and a
Panel subclass
In order to achieve the above implementation, you will have to send a few possible messages from your iframe to your
-
drag to indicate the user has started dragging out of the panel. -
pointermove indicating the user is dragging the content over the canvas at a particular location. -
pointerup indicating the user has dropped the content on the canvas at a particular location. -
cancelDrag indicating the user is no longer dragging content from the panel.
You will also send a message from the
The code of the
// .....
class MyPanel extends Panel {
private readonly viewport: Viewport;
constructor(client: EditorClient, viewport: Viewport) {
super(client, {
title: 'Hello world',
iconUrl: 'https://cdn-cashy-static-assets.lucidchart.com/marketing/images/LucidSoftwareFavicon.png',
location: PanelLocation.RightDock,
url: 'panel.html',
});
this.viewport = viewport
}
protected async messageFromFrame(message: any) {
if (message.message == 'drag') {
const maybeBlock = await this.viewport.startDraggingNewBlock({
className: 'ProcessBlock',
boundingBox: {x: 0, y: 0, w: 200, h: 200},
properties: {'Text': 'Red Square', 'FillColor': '#ff0000'},
});
if (maybeBlock) {
maybeBlock.properties.set('Text', 'I have been placed!');
}
this.sendMessage('dragDone');
} else if (message.message == 'pointermove') {
this.viewport.dragPointerMove(message.x + this.framePosition.x, message.y + this.framePosition.y);
} else if (message.message == 'pointerup') {
this.viewport.dragPointerUp(message.x + this.framePosition.x, message.y + this.framePosition.y);
} else if (message.message == 'cancelDrag') {
this.viewport.cancelDraggingNewBlock();
}
}
}
const panel = new MyPanel(client, viewport);
You can see that
The above example creates a standard process block, but this operation works just as well with custom shapes from your shape libraries:
export class MyPanel extends Panel {
// ...
private scoreBarDefinition:BlockDefinition|undefined;
constructor(client: EditorClient) {
// ...
this.client.getCustomShapeDefinition('shapes', 'score-bar').then(def => {
this.scoreBarDefinition = def;
});
}
//When the app is loaded, it will send a message asking for an update.
protected async messageFromFrame(message: any) {
if (message == 'drag') {
if (this.scoreBarDefinition) {
const maybeBlock = await this.viewport.startDraggingNewBlock(this.scoreBarDefinition);
if (maybeBlock) {
maybeBlock.properties.set('FillColor', '#ff00ffff');
}
}
this.sendMessage('dragDone');
} else if (message.message == 'pointermove') {
this.viewport.dragPointerMove(message.x + this.framePosition.x, message.y + this.framePosition.y);
} else if (message.message == 'pointerup') {
this.viewport.dragPointerUp(message.x + this.framePosition.x, message.y + this.framePosition.y);
} else if (message == 'cancelDrag') {
this.viewport.cancelDraggingNewBlock();
}
}
}
iFrame code
In order to have the html in your iframe move as its being dragged, a few JavaScript functions need to be written. The following code should add the following behaviors:
- The element they drag should move with the mouse cursor when they start dragging.
- The element they drag should disappear from the panel when they move onto the canvas.
- The element they drag should move back to its original location if the user completes or cancels the drag in any way.
<link rel="stylesheet" type="text/css" href="./customUI.css">
<div
class="square"
onpointerdown="pointerDown(event)"
>
Drag me
</div>
<script
type="text/javascript"
src="./customUI.js"
></script>
.square {
width: 100px;
height: 100px;
background: #ff0000;
}
// Event that started the drag (coordinate anchor1).
let pointerDownEvent;
// As of the last pointer event, is the (captured) pointer outside the iframe's bounds?
let pointerIsOut = false;
function pointerDown(event) {
pointerDownEvent = event;
pointerIsOut = false;
startDrag();
};
// Start listening for pointer events on this iframe to implement drag & drop.
function startDrag() {
window.document.addEventListener('pointerup', documentPointerUp);
window.document.addEventListener('pointermove', documentPointerMove);
};
// Cancel drag & drop, and reset the DOM back to how it began.
function stopDrag() {
const target = pointerDownEvent?.target;
if (target instanceof HTMLElement) {
target.style.position = 'static';
target.style.top = '';
target.style.left = '';
pointerDownEvent = undefined;
}
window.document.removeEventListener('pointerup', documentPointerUp);
window.document.removeEventListener('pointermove', documentPointerMove);
stopCanvasDrag();
};
function documentPointerUp(e) {
if(pointerIsOut) {
// Notify the editor that it needs to simulate canvas pointer events.
parent.postMessage({message:'pointerup', x:e.pageX - window.scrollX, y:e.pageY - window.scrollY}, '*');
}
stopDrag();
};
function documentPointerMove(event) {
const isInside = isInsideFrame(event);
if(!pointerIsOut && !isInside) {
startCanvasDrag();
} else if(pointerIsOut && isInside) {
// If the pointer has re-entered the iframe while dragging, tell the extension to
// cancel the ongoing interaction for dragging the new block out.
stopCanvasDrag();
}
pointerIsOut = !isInside;
//While dragging the HTML element around, move it around
//with relative positioning to keep it attached to the pointer cursor.
const target = pointerDownEvent.target;
if (target instanceof HTMLElement) {
target.style.position = 'relative';
target.style.top = event.pageY - pointerDownEvent.pageY + 'px';
target.style.left = event.pageX - pointerDownEvent.pageX + 'px';
}
if(isInside) {
// Defense in depth: If somehow the pointer buttons have been released and the user
// is moving the pointer over this iframe again, cancel any ongoing drag operation.
if (event.buttons == 0) {
stopDrag();
}
}
else {
// Notify the editor that it needs to simulate canvas pointer events.
parent.postMessage({message:'pointermove', x:event.pageX - window.scrollX, y:event.pageY - window.scrollY}, '*');
}
};
function startCanvasDrag() {
parent.postMessage({message: 'drag'}, '*');
window.addEventListener('message', windowMessage);
};
function stopCanvasDrag() {
window.removeEventListener('message', windowMessage);
parent.postMessage({message:'cancelDrag'}, '*');
};
function isInsideFrame(event) {
const x = event.pageX - window.scrollX;
const y = event.pageY - window.scrollY;
return x >= 0 && x <= window.innerWidth && y >= 0 && y <= window.innerHeight;
};
// If you have asked the extension to start a drag-new-block interaction, you need
// to listen for a message indicating that interaction has completed (either
// successfully or not) in order to reset the drag/drop state entirely.
function windowMessage(event) {
if (event.data === 'dragDone') {
stopDrag();
}
};
Lucid styles
You can utilize similar styles of elements that appear in Lucid products in your custom UI by linking the stylesheet at https://lucid.app/public-styles.css, which provides the most up to date styles.
Currently you can style buttons and text fields with Lucid's style patterns:
<html>
<head>
<link type="text/css" rel="stylesheet" href="https://lucid.app/public-styles.css">
</head>
<body>
<button class="lucid-styling primary">primary</button>
<button class="lucid-styling secondary">secondary</button>
<button class="lucid-styling tertiary">tertiary</button>
<input
type="text"
class="lucid-styling"
>
</body>
</html>

Using OAuth APIs
You may need to access OAuth-protected APIs from your editor extensions in order to communicate with other systems or sources of data. In order to do this, you need to:
- Configure the OAuth provider in your
manifest.json . - Upload your extension package to the Developer Portal, and enter the extension ID in your
manifest.json . - Install your extension package for your own user account.
- Enter your OAuth application credentials (client ID and client secret) on the Developer Portal.
- Call
EditorClient.oauthXhr from your editor extension.
Note that while these steps are all mandatory to using an OAuth provider in your extension package once installed, you can also use an OAuth provider in development mode only by following these steps instead:
- Configure the OAuth provider in your
manifest.json . - Create a file
<providerName>.credentials.local in the root of your package, containing a JSON object withclientId andclientSecret keys. - Call
EditorClient.oauthXhr from your editor extension. - Start the local dev server with
npx lucid-package test-editor-extension <extensionName> .
Configure the OAuth provider
Your package's
Field | Description | Example |
---|---|---|
name | A short name by which to reference this OAuth provider | figma |
title | The name displayed to the user to describe this OAuth provider | Figma |
grantType | A GrantType type | authorizationCode |
tokenUrl | The URL for OAuth token exchange | https://www.figma.com/api/oauth/token |
refreshTokenUrl |
|
https://www.figma.com/api/oauth/refresh |
authorizationUrl |
|
https://www.figma.com/oauth |
usePkce |
|
false |
scopes | An array of OAuth scopes to request | ["file_read"] |
domainWhitelist | An array of domains you may make requests to for this provider | ["https://www.figma.com", "https://api.figma.com"] |
clientAuthentication | A ClientAuthentication type | basic |
helpUrl |
|
https://www.example.com/lucid-help |
faviconUrl |
|
https://www.example.com/lucid-favicon.png |
Grant types
This configures the OAuth grant type for the OAuth 2.0 authorization flow.
Type | Description |
---|---|
|
The Authorization Code grant type is used by confidential and public clients to exchange an authorization code for an access token. Learn more |
|
The Client Credentials grant type is used by clients to obtain an access token outside of the context of a user. Learn more |
Client authentication types
This configures how the client ID and client secret should be sent to the OAuth provider when exchanging the authorization code for an OAuth access token.
Type | Description |
---|---|
|
Sends client credentials as Basic Auth header |
|
Sends client credentials in body |
Upload your extension package
The access tokens (and refresh tokens, if available) granted to the user by the OAuth provider are not provided to your editor extension code to use directly. Instead, they are encrypted and stored on Lucid's servers according to our data retention and privacy policies.
Because of this, you must have a version of your extension package installed in order to use OAuth APIs, even in debug and development mode.
Go to the Lucid Developer Portal and create a new extension package project, if you have not already. Copy the extension package's ID into the
Produce the
npx lucid-package bundle
and upload that
Configure Lucid's redirect url in OAuth provider settings
Many OAuth2 APIs require that developers specifically authorize a redirect url before it can be used with their OAuth2 client. Your Lucid extension will use the following redirect url:
https://extensibility.lucid.app/packages/<packageId>/oauthProviders/<oauthProviderName>/authorized
If required by your OAuth provider, configure your client to authorize this redirect url. You will likely have to do this manually in your OAuth client settings.
Enter application credentials
On your extension package's details page on the Developer Portal, a section labeled "OAuth Providers" will appear. It will show that your credentials are missing for your new provider. Click through and enter the client ID and client secret you received from the OAuth provider when setting up your application with them.
These credentials are associated with your extension package's ID and the
Install your extension
Click on the version number of your extension that you just uploaded, and click the button to install the extension package for yourself. This will mark this version as installed and provide a place for OAuth tokens to be stored during your testing.
If you add more OAuth providers later, you need to make sure that you have a version of your extension package installed that includes all the providers you intend to use.
Call the API with EditorClient
In your editor extension, you can request an OAuth API call like this:
const client = new EditorClient();
const result = await client.oauthXhr('sheets', {
url: 'https://www.googleapis.com/oauth2/v1/userinfo',
method: 'GET',
});
The first parameter to
If the user has not yet received an OAuth token from that provider, they will be prompted to authorize with them, and then the requested API call will be made. If the user does not successfully authorize, then
You can also request an OAuth API call using the
Access credentials or tokens
If you need to access the client ID (from your application credentials) in your extension, you can make a request through the editor client like this:
const client = new EditorClient();
const clientId = await client.getOAuthClientId('sheets');
The parameter to the
If you need access to a user's oauth token in your extension, you can make a request through the editor client like this:
const client = new EditorClient();
const token = await client.getOAuthToken('sheets');
The parameter to the
OAuth security
When users authorize your extension to access external data on their behalf, Lucid stores access and refresh tokens for the user in our database. Tokens stored in this manner are encrypted. Account admins on enterprise accounts can configure unique encryption keys used to encrypt tokens for users on their account, learn more about Lucid Key Management Service here.
Data
Add custom data fields to a shape
The easiest and simplest way to add data to a shape is to set custom shape data fields. You can do this by reading and writing from
block.shapeData.set('Value', 50);
const value = block.shapeData.get('Value');
How to create data collections
If you want to have data sets shared between Lucidchart and your own systems, you'll want to have those data sets live independent of the shapes on the canvas. From editor extensions, you can create new collections of data that work like other imported data sets.
These data collections (think of each one as a spreadsheet or database table with a known schema) are organized into data sources. One data source typically represents one set of collections that are all related, e.g. "all the data from this one custom file import".
You can create a data source, collection, and data items like this. Note the second parameter to addDataSource—this is information about where the data came from. You can read this from your extension code in order to find the data source for the import you're interested in working with:
const client = new EditorClient();
const data = new DataProxy(client);
function addData() {
const source = data.addDataSource('my_data_source', {'origin': 'local'});
const collection = source.addCollection('my_collection', {
fields: [
{name: 'id', type: ScalarFieldTypeEnum.NUMBER},
{name: 'name', type: ScalarFieldTypeEnum.STRING},
],
primaryKey: ['id'],
});
collection.patchItems({
added: [
{'id': 1, 'name': 'Ben Dilts'},
{'id': 2, 'name': 'James Judd'},
{'id': 3, 'name': 'Ryan Stringham'},
],
});
}
function findData() {
for (const [key, source] of data.dataSources) {
if (source.getSourceConfig()['origin'] === 'local') {
for (const [collectionId, collection] of source.collections) {
if (collection.getName() === 'my_collection') {
return collection;
}
}
}
}
return undefined;
}
How to associate a record with a shape on-canvas
Once you have a data source and collection with the data you want to use, you can associate an entire record with a shape on-canvas by using a reference key. Reference keys are displayed in Lucidchart near the custom shape data fields.
To create a reference key, you need the ID of the collection and the primary key of the data item you want to link:
block.setReferenceKey('my_reference_key', {
collectionId: collection.id,
primaryKey: '1',
readonly: true,
});
The primary keys of data items are accessible both as the keys on the collection.items, as well as on each item itself:
for(const [primaryKey, item] of collection.items) {
assert(item.primaryKey === primaryKey);
}
Using reference keys to data items is the best long-term way to associate upstream data with a shape in a diagram. This is how we associate source data with shapes in all first-party integrations.
Enabling graphics for shapes connected to data
Once you have shapes connected to data it may be helpful to display the state of the imported data with respect to the external data source. For example, a sync with the external data source may have failed in which case it is desirable to show the user an error state on shapes linked to this data.

Some shapes may display an icon indicating the state of the data connected to it by default, however, for most shapes, this functionality needs to be enabled by the extension:
const position: BadgeEnumPosition = {
horizontalPos: HorizontalBadgePos.LEFT,
verticalPos: VerticalBadgePos.BOTTOM,
layer: BadgeLayerPos.INSIDE,
responsive: BadgeResponsiveness.STACK,
};
block.setDataSyncStateIconPosition(position);
List local data changes
Often, data imported from a third-party source, either using a standard Lucid feature like CSV or Google Sheets import, or via a custom data connector, is editable by the end user. If the data is edited in any way--by editing data-bound text fields, using the Data Panel or other UI, or even via the Extension API--those changes are tracked until they are synced back to the original data source.
To access the list of locally-changed data items on a collection, if any, call CollectionProxy.getLocalChanges. That method returns
Here is some simple code that will examine the currently-selected shape for linked data, then log a full list of all local changes to the associated data collection to the console. If you import an org chart, then make edits to that data by moving people around the organization, adding new people, or removing some people, the
import {EditorClient, Menu, MenuType, Viewport, CollectionProxy} from 'lucid-extension-sdk';
const client = new EditorClient();
const menu = new Menu(client);
const viewport = new Viewport(client);
client.registerAction('dump-changes', async () => {
for (const item of viewport.getSelectedItems()) {
// Look for a data reference associated with a data collection
const collectionId = item.referenceKeys.first()?.collectionId;
if (collectionId) {
// Validate that the reference is a "branch", meaning it is tracking
// local changes independent of the original imported data.
const branch = new CollectionProxy(collectionId, client);
if (branch.getBranchedFrom()) {
const changes = branch.getLocalChanges();
if (changes) {
console.log('ADDED');
for (const item of changes.getAddedItems()) {
console.log(item.primaryKey);
for (const [key, val] of item.fields) {
console.log(key, val);
}
}
console.log('DELETED');
for (const item of changes.getDeletedItems()) {
console.log(item.primaryKey);
for (const [key, val] of item.fields) {
console.log(key, val);
}
}
console.log('CHANGED');
for (const item of changes.getChangedItems()) {
console.log(item.primaryKey);
for (const key of item.changedFields) {
console.log(key, item.original.fields.get(key), item.fields.get(key));
}
}
}
}
}
}
});
menu.addDropdownMenuItem({
label: 'Dump changes',
action: 'dump-changes',
});
Bootstrap data for documents created via API
Our public API for creating a document supports the ability to include bootstrap data that's readable by a specific editor extension.
Alongside the other data you send in the body of the POST, you can include
For example, a full create document request body could look like this:
{
"title": "new document title",
"product": "lucidchart",
"extensionBootstrapData": {
"packageId": "74672098-cf36-492c-b8e6-2c4233549cd3",
"extensionName": "sheets-adapter",
"minimumVersion": "1.4.0",
"data": {
"a": 1,
"b": 2
}
}
}
Because you must specify a
You can then access and process this bootstrap data as follows from your editor extension code:
client.processAndClearBootstrapData((data) => {
// here, data would be {a:1, b:2}
});
In that bootstrap callback, you can operate on the data immediately, or you can do asynchronous operations. If the callback returns a
To verify that bootstrap data processing works, you can directly set the "Bootstrap" property on the document and then refresh the page:
new DocumentProxy(client).properties.set('Bootstrap', {
'PackageId': '74672098-cf36-492c-b8e6-2c4233549cd3',
'ExtensionName': 'sheets-adapter',
'MinimumVersion': '1.4.0',
'Data': {
'a': 1,
'b': 2,
},
});
Marking an extension as required for a document
The
client.processAndClearBootstrapData((data) => {}, true);

The minimum extension version required by document is the
new DocumentProxy(client).properties.set('Bootstrap', {
'PackageId': '74672098-cf36-492c-b8e6-2c4233549cd3',
'ExtensionName': 'sheets-adapter',
'MinimumVersion': '1.4.0',
'Data': {
'a': 1,
'b': 2,
},
});
Lucid cards
Please refer to our Lucid Cards guide for technical walkthrough.
i18n
Any string internationalization or localization must happen inside your extension code. As a convenience, the extension SDK provides access to a full-featured i18n library. This section describes how to use this library, but you are welcome to use any i18n mechanism that works well for you.
Providing and using i18n strings
There is an
i18n.getLanguage | Returns the language code currently selected by the user in Lucid, e.g. "en" or "es-LA" |
i18n.setData | Adds new strings to the currently active i18n dictionary, and updates the active language |
i18n.get | Gets a string from the currently active i18n dictionary and performs interpolations as necessary |
Here is a minimal example of using the library:
const language = i18n.getLanguage();
// Set the English strings as fallbacks, in case other languages
// don't have all the strings translated yet
i18n.setData({
"a": "ay",
"b": "bee",
}, language);
// If the language is set to a language that
// have a dictionary for, set those strings as replacements
if(language == "es-LA") {
i18n.setData({
"a": "ah",
"b": "bay",
}, language);
}
// Later...
const client = new EditorClient();
client.alert(i18n.get("a"));
Use JSON files for string dictionaries
Inlining your i18n strings into your source code isn't a scalable solution. Instead, you can put your dictionaries into
import en from '../resources/i18n/en.json';
import de from '../resources/i18n/de.json';
import esLa from '../resources/i18n/es-LA.json';
const language = i18n.getLanguage();
i18n.setData(en, language);
if (language == 'de') {
i18n.setData(de, language);
} else if (language == 'es-LA') {
i18n.setData(esLa, language);
}
In order to import JSON files like this, you'll need to make sure your
declare module '*.json' {
const content: Record<string, string>;
export default content;
}
You must also have
String interpolation
You can mark sections of your string to be replaced with curly braces:
i18n.setData(
{
'title': 'Send an email to {name}',
},
i18n.getLanguage(),
);
// "Send an email to Ben"
console.log(i18n.get('title', {'name': 'Ben'}));
Pluralization
You can provide entries in your i18n dictionary that support pluralization by appending (in most languages)
i18n.setData(
{
'title.one': 'Send an email to {count} person',
'title.other': 'Send an email to {count} people',
},
i18n.getLanguage(),
);
// "Send an email to 1 person"
console.log(i18n.get('title', {'count': 1}));
// "Send an email to 4 people"
console.log(i18n.get('title', {'count': 4}));
You can learn more about pluralization rules on this site from the Unicode CLDR Project. You can see a full list of which pluralization rules you should specify for every possible language here.
For convenience, here is the list of pluralization suffixes you should provide (only for strings interpolating a
Language | Suffixes |
---|---|
Chinese | other |
Dutch | one, other |
English | one, other |
French | one, other |
German | one, other |
Italian | one, other |
Japanese | other |
Korean | one, other |
Polish | one, other |
Portuguese | one, other |
Russian | one, few, many |
Spanish | one, other |
Swedish | one, other |
Lists of strings
You can provide a list of strings for expansion into a comma-separated list.
i18n.setData(
{
'shopping-list': 'Purchase {shoplist} today',
},
i18n.getLanguage(),
);
// "Purchase bread today"
const shoppingList1 = ['bread'];
console.log(i18n.get('shopping-list', {'shoplist': shoppingList}));
// "Purchase bread, apples, fish, and butter today"
const shoppingList2 = ['bread', 'apples', 'fish', 'butter'];
console.log(i18n.get('shopping-list', {'shoplist': shoppingList2}));
By default, the list expansion will add commas and will default to
List Style | Example |
---|---|
LONG | a, b, and c |
SHORT | a, b, & c |
NARROW | a, b, c |
List Type | Example |
---|---|
AND | a, b, and c |
OR | a, b, or c |
UNIT_LIST | a b c |
If the text will change based on the length of the list, it can be combined with the Pluralization rules by also providing a
i18n.setData(
{
'invite-list.one': 'You might invite your friend {friendlist} to the party',
'invite-list.many': 'You might invite your friends, {friendlist}, to the party',
},
i18n.getLanguate(),
);
// "You might invite your friend Anna to the party"
const friendList1 = ['Anna'];
console.log(i18n.get('invite-list', {'count': friendList1.length, 'friendlist': i18n.formatList(friendList2, {style: ListStyles.LONG, type: ListTypes.OR})}));
// "You might invite your friends, Anna, Betty, Carl, or Dave, to the party"
const friendList2 = ['Anna', 'Betty', 'Carl', 'Dave'];
console.log(i18n.get('invite-list', {'count': friendList2.length, 'friendlist': i18n.formatList(friendList2, {style: ListStyles.LONG, type: ListTypes.OR})}));
Wrapped strings
You can "wrap" sections of your i18n strings in markup, often used for producing HTML links and other simple markup:
i18n.setData(
{
'message': 'Send an email to <w0>{name}</w0>',
},
i18n.getLanguage(),
);
// Send an email to <a href="mailto:ben@example.com">Ben</a>
console.log(i18n.get(
'message',
{'name': 'Ben'},
['<a href="mailto:ben@example.com">{}</a>']
));
Here, you're replacing the first wrapping tag
Supported languages
Language | Code |
---|---|
Chinese (simplified) | zh-CN |
Chinese (traditional) | zh-TW |
Dutch | nl |
English | en |
French | fr-FR |
German | de |
Italian | it-IT |
Japanese | ja |
Korean | ko |
Polish | pl-PL |
Portuguese | pt-BR |
Russian | ru |
Spanish | es-LA |
Swedish | sv-SE |
Package settings
Extension packages can specify settings to be configured by the end user who installs the package. The user will be prompted to enter values for these setting during installation. The values of these settings are shared for all users of each installation of your package (typically all users on the Lucid account).
Settings entries in manifest.json
The package manifest file can optionally include an array of settings, each of which has the following fields:
name | A string uniquely identifying this setting within this package |
label | A human-readable string label for this setting, which will be used in the default UI for configuring your package |
description | A longer description for the setting, to be displayed alongside the label where appropriate |
type | The type of the setting; currently the only legal value is |
default | Optional: A default value. If set, the user will not be prompted to fill this field out at installation time |
For example, if you wanted to allow the user to be able to configure a subdomain of your app to connect to for data, you might add the following to your manifest:
{
// ...
"settings": [{
"name": "subdomain",
"label": "MyApp Subdomain",
"description": "The subdomain of your instance of MyApp. If you log in to MyApp at company.example.com, you should enter 'company' for your MyApp Subdomain.",
"type": "string"
}]
}
You can then use the user provided value for this setting within the OAuth provider section or the data connector URL. For instance, in the example below,
{
// ...
"oauthProviders": [
{
"name": "provider",
"title": "Provider",
"authorizationUrl": "https://{{=@subdomain}}.example.com/-/oauth_authorize",
"tokenUrl": "https://{{=@subdomain}}.example.com/-/oauth_token",
"scopes": ["default"],
"domainWhitelist": ["https://{{=@subdomain}}.example.com"],
"clientAuthentication": "clientParameters"
}
],
}
Accessing the configured values
You can query the user's configured values for your package's settings from your editor extension code, check their permissions to edit those values (only users with permissions to manage the extension installation can edit setting values), and even prompt the user to edit their settings:
import {EditorClient, Menu, MenuType} from 'lucid-extension-sdk';
const client = new EditorClient();
const menu = new Menu(client);
client.registerAction('import-data', async () => {
let settings = await client.getPackageSettings();
if (!settings.get('subdomain')) {
if (await client.canEditPackageSettings()) {
await client.alert(
'You have not configured a MyApp subdomain. You will now be prompted to complete that configuration.',
);
await client.showPackageSettingsModal();
settings = await client.getPackageSettings();
if (!settings.get('subdomain')) {
return;
}
} else {
client.alert(
'Your account has not configured a MyApp subdomain. Talk with your Lucid account administrator to complete configuration of the MyApp integration.',
);
}
}
// Do whatever you need with the configured settings
});
menu.addDropdownMenuItem({
label: 'Import data',
action: 'import-data',
});
You can programmatically set package settings using
Shape libraries
This section has information on how to add a shape library to your application. You can learn more about how to design custom shapes to go in your library here.
Shape libraries can be created automatically using the lucid-package CLI tool:
/my-package$ npx lucid-package create-shape-library my-shape-library
Shape library file structure
Shape libraries are structured like this:
> shapelibraries
> my-shape-library
> images
└── ...
> shapes
└── ...
└── library.manifest
- The
images folder is where you will add images that will be used by shapes in the library. - The
shapes folder contains all the.shape files for your shape library. You can learn more about.shape files here. - The
library.manifest file declares all of your shapes, as well as some defaults for each shape like its height and width. You can learn more about shape library manifests here.
Shape library manifest
To add a shape library to your extension package, you will need to declare it in your
Here is an example of what a shape library manifest entry might look like:
{
// ...
"shapeLibraries": [
{
"name": "my-shape-library",
"product": "chart",
"lcszPath": "shapelibraries/my-shape-library.lcsz"
}
]
}
A shape library manifest should have the following fields:
-
name : The name of the shape library. This should match the name of the folder that contains the shape library. -
product : Which product this shape library is for. Currently, only Lucidchart ("product": "chart" ) is supported. -
lcszPath : The file path to the entry point of your shape library. By default, this will beshapelibraries/<name>.lcsz . This file is created when you bundle your package.
Data connectors
Data connectors can be created automatically using the create-data-connector command of the lucid-package CLI tool:
$ npx lucid-package create-data-connector my-data-connector
Data connector file structure
A data connector is structured like this:
> my-data-connector
> actions
└── ...
└── index.ts
└── debug-server.ts
└── package.json
└── tsconfig.json
- The
actions folder is where the implementation for each of your action handlers will go. These are called when your editor extension callsperformDataAction . -
index.ts is the entry point for your data connector. This is where you will define which actions your data connector supports. -
debug-server.ts is a utility for running your data connector locally during development. -
package.json andtsconfig.json define environment settings for your data connector.
Data connector manifest
To add a data connector to your extension package, you will need to declare it in your
Here is an example of what a data connector manifest entry might look like:
{
// ...
"dataConnectors": [
{
"name": "my-data-connector",
"oauthProviderName": "oauth",
"callbackBaseUrl": "https://www.example.com/",
"dataActions": {
"Import": "import"
}
}
]
}
A data connector manifest should have the following fields:
-
name : The name of the data connector. This will be referenced by your editor extension code to specify which data connector you are using for a request. -
oauthProviderName : The name of an OAuth provider that is defined in the extension package. When a request is made to the data connector, it will include the OAuth access token for the user who initiated the request so that your data connector can query an OAuth API on behalf of the user. -
callbackBaseUrl : The base URL your data connector is hosted at. -
dataActions : The data actions that your data connector supports, and the url suffix that should be added to the base url when the data action is invoked by the editor extension. In the above example, the data connector supports one action (Import), and requests will be made tohttps://www.example.com/import when the editor extension invokes that action.
Building a data connector
You may need to access data from a source not already supported by Lucid. In order to do this, you have two choices:
- Fetch data using
EditorClient.oauthXhr and add it to the document by creating data sources and collections in your extension as described here. - Create a data connector which can access and manage data in response to requests from Lucid's servers, and add the data connector to your extension.
For simple integrations, the first approach may be entirely sufficient. However, if you want your data to update when changes are made to it in Lucid documents, or if you want updates that happen in the data's source to be automatically reflected in Lucid documents, then you'll need to create a data connector.
A data connector is a collection of callback endpoints that translate between external representations of data and Lucid's representation. Data connectors are bidirectional, allowing for data updates to flow from external sources into Lucid documents and from Lucid documents back to their external sources. A data connector must provide at least one URL at which it can respond to requests made by Lucid's servers.
When Lucid makes a request to your data connector, it will come in the form of a data action. Data actions are triggered either explicitly by code in your extension, or automatically based on user interactions with Lucid documents. Each data action has a name, a callback url suffix, an OAuth token for accessing the data on behalf of the requesting user, and other information that is relevant to fulfilling the user's request.
To add a data connector to your extension you must:
- Declare your data connector in your
manifest.json . - Implement your data connector.
- Expose a URL Lucid's servers can use to make requests to your data connector.
Declare your data connector
To declare a data connector, include an array of
Field | Description | Example |
---|---|---|
name | Then name you will use to refer to this data connector in your extension | DemoDataConnector |
oauthProviderName | The name of an OAuth provider defined in your |
asana |
callbackBaseUrl | The base url Lucid will send callback events to | https://www.example.com/ |
dataActions | A mapping of the data actions supported by this data connector to the url suffix that should be appended to the callbackBaseUrl when making requests for that type of data action. | {"Import" : "import"} |
Any time you add or update declarations for any data connectors, you will need to package and upload your manifest, then install the extension for yourself again before your data connector can be used by your extension.
Implement your data connector
As an example, let's start with an "Import" data action which instructs the data connector what data to import onto a document.
To trigger an "Import" data action from your extension, call
const client = new EditorClient();
client.performDataAction({
dataConnectorName: 'DemoDataConnector',
actionName: 'Import',
actionData: {'requestedItems': ['id-1', 'id-2']},
asynchronous: true,
});
If your
For your convenience, the
new DataConnector(new DataConnectorClient(cryptoDependencies)).defineAsynchronousAction('Import', async (action) => {
const client = action.client; // <- an authorized client for sending data back to the document
const actionName = action.name; // <- "Import"
const actionData = action.data; // <- {'requestedItems': ['id-1', 'id-2']}
const userCredential = action.context.userCredential; // <- the OAuth access token for the user who triggered the data action
});
To add a new collection to the document containing the requested items, you must first define what the data will look like using a schema. For this example, you will use data of the following type:
type MyItemType = {id: string; name: string; age: number; isSingle: boolean};
You can define the schema for your data by calling
const myCollectionSchema = declareSchema({
primaryKey: ['id'],
fields: {
'id': {type: ScalarFieldTypeEnum.STRING},
'name': {type: ScalarFieldTypeEnum.STRING},
'age': {type: ScalarFieldTypeEnum.NUMBER},
'isSingle': {type: ScalarFieldTypeEnum.BOOLEAN},
},
});
// Infer the TS type:
type MyItemType = ItemType<typeof myCollectionSchema.example>;
let dataItem: MyItemType = {id: 'id-1', name: 'John', age: 30, isSingle: true};
To fulfill the import request, retrieve the data that was requested, then use the authorized client provided to post the data back to the document:
const makeDataConnector(
client: DataConnectorClient,
) => {
return new DataConnector(client).defineAsynchronousAction('Import', async (action) => {
const itemsToAdd: MyItemType[] = [
{id: 'id-1', name: 'John Doe', age: 30, isSingle: true},
{id: 'id-2', name: 'Jane Doe', age: 31, isSingle: true},
];
action.client.update({
dataSourceName: 'Demo Data Source',
collections: {
'My Collection': {
schema: {
fields: myCollectionSchema.array,
primaryKey: myCollectionSchema.primaryKey.elements,
},
patch: {
items: myCollectionSchema.fromItems(itemsToAdd),
},
},
},
});
});
}
Field labels
When fields are displayed in the card details panels, or in other visualizations, the field name will be used as the label for the field. You can specify the labels that will be used by providing a custom label as part of the schema while adding a collection. This is done by adding a collection to a data source that has been imported. Include the fields from the source you want to display, specify which field is the primary key, then add field labels for any field names you want to override:
source.addCollection('track-tickets', {
fields: [
{name: 'id', type: ScalarFieldTypeEnum.STRING},
{name: 'description', type: ScalarFieldTypeEnum.STRING},
{name: 'assigned', type: [ScalarFieldTypeEnum.STRING, ScalarFieldTypeEnum.NULL]},
{name: 'state', type: [ScalarFieldTypeEnum.STRING, ScalarFieldTypeEnum.NULL]},
],
primaryKey: ['id'],
fieldLabels: {
'id': 'Ticket',
'description': 'Description',
'assigned': 'Assigned',
'state': 'State',
},
});
Expose a URL for your data connector
The final step is to use your data connector to handle incoming requests by calling
const response = await dataConnector.runAction(requestUrl, requestHeaders, requestBodyAsJson);
As an example, this code sets up a simple development server using node express that will accept and respond to requests made to your data connector:
import {DataConnectorClient} from 'lucid-extension-sdk';
import {makeDataConnector} from './dataconnector';
import * as express from 'express';
import * as crypto from 'crypto';
const client = new DataConnectorClient({Buffer, crypto});
const dataConnector = makeDataConnector(client);
dataConnector.runDebugServer({express});
node ./serve.js
You will need to expose the URL you are using to accept requests publicly so that Lucid's servers can send it requests.
Examples
For a more thorough example, you can look at the source code for Lucid's Asana Cards extension here. You can find the extension here if you want to try it out.
Data actions
Data actions are the backbone of a data connector. Data actions are invoked either explicitly by your editor extension, or in some cases automatically by Lucid’s server to help sync data changes. Data connectors declare which data actions they support, including things like importing data, refreshing data on open Lucid documents, and patching data changes made in Lucid back to an external data source.
Some actions are reserved, meaning that they are called outside of your extension when certain criteria are met. All other actions are custom, which means you are responsible for calling them somewhere within your extension. Here is a list of the current reserved actions:
Action | Description |
---|---|
HardRefresh | Called after a document is closed for an extended period of time to refresh data on the document |
Poll | Called every 30 seconds when a document is open to refresh data on the document |
Patch | Called when users make changes to data on Lucid documents so those changes can be sent to the external data source |
Registering data actions
Each action that your data connector supports needs to be declared within the data connector section of your
{
// ...
"dataConnectors": [
{
"name": "my-data-connector",
"oauthProviderName": "...",
"callbackBaseUrl": "...",
"dataActions": {
"MyAction": "MyAction",
"AnotherAction": "AnotherAction"
// ...
}
}
]
}
You also have to append the actions to your data connector’s
import {DataConnector, DataConnectorClient} from 'lucid-extension-sdk';
import {myAction} from './actions/myaction';
import {anotherAction} from './actions/anotheraction';
export const makeDataConnector = (client: DataConnectorClient) =>
new DataConnector(client)
.defineAsynchronousAction("MyAction", myAction)
.defineAsynchronousAction("AnotherAction", anotherAction)
Calling data actions
After actions are created, you need to call them somewhere in your extension (unless they are called automatically by the SDK). This is done by using the
await editorClient.performDataAction({
actionName: "MyAction"
actionData: <Data>
// ...
});
Import action
An action you will likely want to create is an import action which pulls data from an external source, and posts that data to Lucid so it can be displayed on a Lucid document. How this is done will vary, but it might look something like this:
{
// ...
"dataConnectors": [
{
// ...
"dataActions": {
"Import": "Import"
}
}
]
}
import {DataConnector, DataConnectorClient} from 'lucid-extension-sdk';
import {importAction} from './actions/importaction';
export const makeDataConnector = (client: DataConnectorClient) =>
new DataConnector(client)
.defineAsynchronousAction("Import", importAction)
import {DataConnectorAsynchronousAction} from 'lucid-extension-sdk';
// You would define these variables/methods depending on the structure of your data
import {CollectionName} from '...';
import {getFormattedCollection, collectionSchema} from '...';
export const importAction: (action: DataConnectorAsynchronousAction) => Promise<{success: boolean}> = async (
action,
) => {
const apiClient = new APIClient(...);
// action.data will contain data passed to the data connector by your editor extension
// It will be up to you to determine what data you need
const collectionIds = action.data as string[];
// Fetch the data
const fullCollectionData = await apiClient.getCollections({ids: collectionIds});
// Convert the data into a Lucid compatible format
const formattedCollectionData = fullCollectionData.map(getFormattedCollection);
// Send the imported data to Lucid
await action.client.update({
dataSourceName: 'dataSource',
collections: {
[CollectionName]: {
schema: {
fields: collectionSchema.array,
primaryKey: collectionSchema.primaryKey.elements,
},
patch: {
items: collectionSchema.fromItems(formattedCollectionData),
},
},
},
});
return {success: true};
};
Hard refresh action
Hard refresh is called when a document is opened after it has been closed for more than 5 minutes. Your hard refresh action should fetch the data that has already been imported into Lucid by using
{
// ...
"dataConnectors": [
{
// ...
"dataActions": {
"HardRefresh": "HardRefresh" // NOTE: since this is a reserved action, the name must be "HardRefresh"
}
}
]
}
import {DataConnector, DataConnectorClient} from 'lucid-extension-sdk';
import {hardRefreshAction} from './actions/hardrefreshaction';
export const makeDataConnector = (client: DataConnectorClient) =>
new DataConnector(client)
.defineAsynchronousAction("HardRefresh", hardRefreshAction)
import {DataConnectorAsynchronousAction} from 'lucid-extension-sdk';
import {isString} from 'lucid-extension-sdk/core/checks';
// You would define these variables/methods depending on the structure of your data
import {CollectionName} from '...';
import {getFormattedCollection, collectionSchema} from '...';
export const hardRefreshAction: (action: DataConnectorAsynchronousAction) => Promise<{success: boolean}> = async (
action
) => {
const apiClient = new APIClient(...);
// Find the data that is already on the document
let collectionIds: string[] = [];
Object.keys(action.context.documentCollections).forEach((key) => {
if (key.includes('Collection')) {
collectionIds = collectionIds.concat(
action.context.documentCollections?.[key].map((collectionId) => JSON.parse(collectionId)).filter(isString),
);
}
});
// If there is no data, you shouldn't need to update anything
if (collectionIds.length == 0) {
return {success: true};
}
// Fetch the data of the collections on the document
const fullCollectionData = await apiClient.getCollections({ids: collectionIds});
// Convert the data into a Lucid compatible format
const formattedCollectionData = fullCollectionData.map(getFormattedCollection);
// Send the updated collections to Lucid
await action.client.update({
dataSourceName: 'dataSource',
collections: {
[CollectionName]: {
schema: {
fields: collectionSchema.array,
primaryKey: collectionSchema.primaryKey.elements,
},
patch: {
items: collectionSchema.fromItems(formattedCollectionData),
},
},
},
});
return {success: true};
};
Poll action
The poll action refreshes all of the data on a Lucid document periodically. Polling achieves the same thing as the hard refresh action, but runs every 30 seconds while the Lucid documents are open. In most cases you can reuse the code you wrote for the hard refresh action, as both strategies update your data the same way. The poll action is distinct in that it can refresh data on multiple documents at once, whereas the hard refresh action only refreshes data on a single document. If you have implemented the Hard Refresh action, your code might look something like this:
{
// ...
"dataConnectors": [
{
// ...
"dataActions": {
"Poll": "Poll" // NOTE: since this is a reserved action, the name must be "Poll"
}
}
]
}
import {DataConnector, DataConnectorClient} from 'lucid-extension-sdk';
import {hardRefreshAction} from './actions/hardRefreshAction';
export const makeDataConnector = (client: DataConnectorClient) =>
new DataConnector(client)
.defineAsynchronousAction("Poll", hardRefreshAction)
Data connector security
Data connectors and the data they manage are secured through two means:
- Request signatures to validate requests to your data connector originated from Lucid's servers.
- Data update tokens authorize your data connector to update specific Lucid documents with data.
Because data connectors use OAuth 2.0 user access tokens to communicate with third parties, make sure you're also aware of how we secure OAuth 2.0 tokens.
Request signatures
For your security, Lucid provides a request signature header (
Validation
If you use the
If you would like to validate the requests yourself, you can follow this
const crypto = require('crypto');
const parts = request.uri.split('?')
const params = parts.length > 0 ? parts[1] : "";
const nonce = request.headers['X-Lucid-RSA-Nonce'];
const signature = Buffer.from(request.headers["X-Lucid-Signature"], "base64");
const data = Buffer.from(body + nonce + params);
const verified = crypto.verify("SHA384", data, LUCID_PUBLIC_KEY, signature);
Data update tokens
When a data connector recieves a data action request from Lucid, it will usually contain a data update token which can be used to send data back to Lucid documents. The data update token is scoped in one of two ways, depending on the type of data action that was invoked:
- Document Specific - The data update token is scoped to a particular document and data source. In this case, the token can be used to add, change, or delete any data for the data source on the document. The data update token can also create a data source or new collections if they don't already exist on the document.
- Data Source Specific - The data update token is scoped to existing data for a particular data source across multiple Lucid documents. In this case the data update token can be used to add data to or change data in existing collections on those documents. However, the token cannot be used to create new collections or new data sources on the document.
The following table shows which type of data update token each data action will recieve:
Data Action Name | Update Token Type |
---|---|
Import | Document Specific |
Hard Refresh | Document Specific |
Patch | None |
Poll | Data Source Specific |
Custom Synchronous Action | None |
Custom Asynchronous Action | Document Specific |
lucid-package CLI
lucid-package is a command-line interface for creating and managing extension packages. The getting started section goes over the very basics of using
To install
This section describes each command available in the
create
npx lucid-package create <extension-name>
Use
create-editor-extension
npx lucid-package create-editor-extension <name>
This command creates a new directory with the given name, with a very simple editor extension inside it. Your
You should adjust the
create-data-connector
npx lucid-package create-data-connector <name>
This command creates a new directory with the given name in the data-connectors folder of your extension package. The new data connector comes with some skeleton code, and your
To use your data connector you must add an OAuth provider and update the
You will also need to declare which data actions your data connector will support in the
Finally, you will need to set a
npx nodemon debug-server.ts
create-shape-library
npx lucid-package create-shape-library <name>
This command creates a shape library in a new directory with the given name, and adds a reference to that shape library in
For more information, see the Shape Libraries section in the developer guide.
create-image-shape-library
npx lucid-package create-image-shape-library <name> <image-path>
This command creates a shape library from a directory of images. Every file in the target folder will become a new shape with a corresponding image. Width and height for each image will be read in automatically.
You can add the tag
You can then test normally with test-shape-libraries or test-editor-extension command.
test-editor-extension
npx lucid-package test-editor-extension <name>
This starts a local HTTP server that watches the given editor extension's source code and provides a debug-compiled version of it to the Lucid product specified for that editor extension in
See Getting started for more information on debugging editor extensions.
This command also serves all shape libraries in your extension package as well, as often those shape libraries are needed by the editor extensions to work correctly. See test-shape-libraries for more details.
test-shape-libraries
npx lucid-package test-shape-libraries
This starts a local HTTP server that watches this extension's shape library source code and provides a debug-compiled version of it to the Lucid product specified for those shape libraries in
While running this command, all shape libraries in this extension are automatically activated and loaded into the shape toolbox. This is not the case when an end user installs your extension, but is provided as a convenience to make it easier and faster to test changes to shape libraries.
Changes to shape code should be reflected almost immediately, without a need to refresh the browser.
bundle
npx lucid-package bundle
The
Multiple environments
The
npx lucid-package bundle --env=staging
This example will mix in the values from
build-editor-extension
npx lucid-package build-editor-extension <name>
After creating an editor extension as part of your extension package,
This command is not necessary to use directly, as the bundle command already compiles your extensions, but may be useful as a troubleshooting tool.
update-sdk
npx lucid-package update-sdk
This command updates the npm dependency of each of your editor extensions to the latest published version of lucid-extension-sdk. You can also do this yourself by going into the directory of each editor extension and running
Release notes
lucid-extension-sdk
0.0.242
You can now use an array of strings in an `i18n`` string.
0.0.217
You can now add and remove columns and rows using the TableBlockProxy class.
0.0.209
There are new, more specific functions for adding menu items. Extensions can add menus to the top level menus, context menu, and left side content dock in Lucid Spark. Instead of one entry point for all these there are now three different functions each with specific parameters: addDropdownMenu, addContextMenu, addContentDockMenu.
0.0.173
You can now specify
0.0.140
You can now extend CustomBlockProxy for custom shapes defined in your extension package, and that class will automatically be used instead of
For more details, see Identifying custom shapes.
0.0.134
You can now programmatically examine data changes made locally that have not yet been synced back to the original source.
0.0.106
i18n support was just added to extensions, with the recommendation being to store your strings in JSON files under
If you want to add i18n to an editor extension created before this update, make sure you add
module.exports = {
entry: './src/extension.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /[\\\/]resources[\\\/]/,
use: 'raw-loader',
exclude: /\.json$/, // <- Newly added
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'bin/extension.js',
path: __dirname,
},
mode: 'development',
};
You also need to make sure
declare module '*.json' {
const content: Record<string, string>;
export default content;
}
0.0.84
You can now check which Lucid product is currently loaded with EditorClient.getProduct.
0.0.27
Custom panels can now be placed into the toolbox area on the left of Lucidchart by specifying PanelLocation.ContentDock.
0.0.26
If a data URL is provided as an image fill style for a block, that image will be uploaded to Lucid's image service on the current user's account to optimize performance.
0.0.25
Extensions can now read, set, or clear simple static icon data graphics on shapes using BlockProxy.getSimpleStaticDataGraphic and BlockProxy.setSimpleStaticDataGraphic. For example:
const client = new EditorClient();
const viewport = new Viewport(client);
client.registerAction('toggleIcon', () => {
const selection = viewport.getSelectedItems();
for (const block of selection) {
if (block instanceof BlockProxy) {
const icon = block.getSimpleStaticDataGraphic();
if (icon) {
block.setSimpleStaticDataGraphic(undefined);
} else {
block.setSimpleStaticDataGraphic({
icon: {
set: DataGraphicIconSets.STATUS_ICONS,
index: 0,
},
position: {
horizontalPos: HorizontalBadgePos.RIGHT,
verticalPos: VerticalBadgePos.TOP,
layer: BadgeLayerPos.EDGE,
responsive: BadgeResponsiveness.STACK,
},
color: '#ff0000',
});
}
}
}
});
0.0.24
Extensions can now easily read, set, or clear drop shadows on shapes using BlockProxy.getShadow and BlockProxy.setShadow.
0.0.21
Custom panels can now allow users to drag and drop blocks from the custom panel onto the current page. See the new section Drag and drop blocks from a panel for details.
0.0.20
Extension can now mark themselves as required for a document. See the new section Marking an extension as required for a document for details.
0.0.15
Menu items can now prompt the user to select files to be read by your extension. See the new section Custom file upload in the Developer Guide.
0.0.14
The EditorClient class now has new
0.0.13
The Viewport class added the ability to hook the selection changing.
0.0.12
The Viewport class added the ability to hook text editing. See the new section Hook Text Editing in the Developer Guide.
0.0.11
The DocumentProxy class added methods to hook creating new items (blocks, lines, and groups) on the document.
The BlockProxy class added a
0.0.10
The LineProxy class added methods to add text areas as well as read and write the position of existing text areas on the line. See the new section on Managing text in the Developer Guide.
0.0.9
The Panel class was added, allowing the creation of custom UI in the right dock area of Lucidchart. See the new section on Custom UI in the Developer Guide. In order to use this new functionality you need to add the
0.0.8
The EditorClient class now has a new method,
lucid-package
0.0.79
Added support for default values for package settings.
0.0.72
Added support for serving content from a
0.0.31
When using
0.0.29
When using
For example, if you want to use a different
{
"dataConnectors": [
{
"callbackBaseUrl": "https://debug-data-connector.localhost/connector-name/"
}
]
}
This allows you to always keep production configuration values in your
0.0.26
The OAuth 2.0 client credentials grant type is now supported.
0.0.24
OAuth providers can now be used in development mode without a need to bundle and install the package. Add a JSON file named
0.0.23
New packages created with the CLI now include a
0.0.20
Custom shape libraries may now include multiple entries in the library manifest referring to the same shape definition file. Shape entries in the shape library manifest may now optionally include a "key" to be used to differentiate between them when calling EditorClient.getCustomShapeDefinition.
0.0.14
Dev server is more fault-tolerant while starting up.
0.0.13
Bugfix: Dev server now sends the correct package ID and version for shape libraries.
0.0.11
Editor extension manifests may now specify the
0.0.9
Package manifests may now specify
0.0.8
Package manifests may now include two additional fields,
If specified,
For specifying bootstrap data on documents created via the Lucid OAuth API, you must now use the package
When doing local development of an editor extension, if you already have another version of that extension installed, the local code will be loaded instead of the installed version.