Welcome
Welcome to the Lucid developer platform! Lucid provides powerful APIs that enable developers to add the power of visual collaboration to their product or workflow. This section serves as an introduction to what's possible and how to start building.
Use cases
Developers can build multiple kinds of apps on our platform. Below are examples of how you can use our APIs:
- External embed of Lucid documents: Display diagrams and whiteboards created with Lucid in other websites.
- On-canvas link unfurl: Visualize content from other platforms within Lucid products.
- Task management cards: Provide a way to visually create and track tasks across the entire project workflow.
- Shape libraries: Distribute custom icons and shapes across an organization or globally on the Lucid marketplace.
- Data import/syncing: Update Lucid diagrams with data from outside sources and make changes to data sources directly from Lucid documents.
APIs
Lucid offers three sets of APIs:
- Lucid's REST API allows developers to programmatically interact ( create, search, read contents of, trash, etc.) documents and folders. This API also provides endpoints for embedding documents, adding/removing collaborators, transfering content between users, and more.
- Lucid's Extension API allows developers to add functionality to Lucid editors. This API can import data, add/read/modify shapes and lines on the canvas, and define new shape libraries for internal or public distribution.
- Lucid's SCIM API offers user provisioning and group management, available for admins on an Enterprise account.
Start building
Building on the developer platform requires access to developer tools. To get access, read Unlocking developer tools.
To start building on the REST API, go to the OAuth 2.0 client creation guide for help creating a client, then visit the Using OAuth 2.0 section in our REST API technical documentation to create an access token needed for API requests.
To start building on the Extension API, see Getting started in our Extension API technical documentation. For examples of extensions built on the Extension API, see Lucid's public GitHub repository.
To start building on the SCIM API, see our SCIM technical documentation.
Partner with us
Looking to build apps and integrations on Lucid's platform? Sign up to be a technology partner! We'd love to learn more about how you use Lucid and take our partnership to the next level. Partnership benefits may include:
- Co-marketing support
- Sales and support enablement
- Listing and premium placement on the Lucid integrations marketplace
- Collaboration with Lucid’s product team
- And much more
Get support
If you have questions or need support, please contact us in our Lucid developer forum.
Request feature
Submit feedback and feature requests through our feedback form.
Unlocking developer tools
What are developer tools?
Lucid provides developer tools that allow you to create, test, and distribute apps built on Lucid’s REST and Extension APIs:
- The developer portal allows you to create new applications. An application contains extension packages built using Lucid’s Extension API and/or an OAuth 2.0 client needed to use Lucid’s REST API. The portal is also where you distribute apps.
- The developer menu allows you to test apps built on Lucid's Extension API.
Users without these tools are unable to build or distribute code with these APIs.
How do I unlock developer tools?
There are two ways to unlock developer tools:
- Via User Setting: Check the “Enable developer tools” checkbox in user settings. Please note the checkbox may be disabled due to account settings controlled by your Lucid Admin.
- Via your Lucid Admin: If the “Enable developer tools” checkbox is disabled, ask your Lucid Admin to assign you the developer role.
Via User Setting
The “Enable developer tools" checkbox is found in user settings (see GIF below). You can find user settings in your account settings (accessible in the upper right of lucid.app). Please note that admins are able to enable/disable your ability to unlock developer tools in user settings (instructions to do so here).
Via Admin
Lucid admins are able to assign a user the developer role (regardless of whether the “Enable developer tools" checkbox is enabled or disabled). Assigning a user the developer role unlocks developer tools for that user. Admins can assign a user the developer role by navigating to the users section of the admin settings, selecting a user, and editing their roles.
Also note that assigning a user the developer role unlocks developer tools for that user, even if they haven’t enabled developer features in their user settings.
Additional Resources
- Admin controls for developer tools
- Application collaborator roles
- How to create an OAuth 2.0 client
- Code for example extension apps in Lucid's repository of sample Lucid extensions
Application Collaborator Roles
What is an application collaborator role?
Application collaborator roles enable you to manage which other Lucid users can test, develop, publish, and maintain your app.
At a high-level, the purpose of each role is:
- App owners can be considered an "app admin". App owners have full permissions to the app, including viewing the app's OAuth 2.0 client secret or publishing the app for the first time.
- The code editor role is designed for the developers actually coding the app. They can upload new versions of the app to the developer portal and view the client ID and redirect URLs.
- The listing editor role is designed for marketing your app on the Lucid Marketplace. Listing editors can edit the marketplace listing page's content and use any uploaded version of the app to collect screenshots if needed. However, listing editors don't have development-related permissions: they can neither upload package versions nor see the OAuth 2.0 client ID or secret.
- The tester role is designed for those testing your app. Or if you don't intend to publish your app, this role can also be used to distribute your app to select users. The only permission a tester has is to use the app before it's published.
In order to preserve the security and integrity of your app and its marketplace listing, app owners, code editors, or list editors need to be part of your company's Lucid account. To facilitate testing pre-release versions of your app with users at a different company, you can invite any Lucid user to be a tester on your app.
The sections below contains a summary of each application collaborator role and its associated permissions.
Summary of application collaborator roles
App owner
- Create, view, and manage OAuth 2.0 credentials.
- Request app be listed in Lucid marketplace.
- Request app be listed in the company-only marketplace.
- Upload new versions of the app.
- Choose the app version published to the marketplace.
- View the app's marketplace listing.
- Edit the app's marketplace listing.
- Use any uploaded app version before it's published.
- Invite/remove collaborators at the same company with the following roles: app owner, code editor, listing editor, tester.
- Invite/remove collaborators at a different company with the following role: tester.
- Automatically get access to both the developer portal and the developer menu unless an account admin has disabled self-selecting into the developer role.*
Code editor
- View OAuth 2.0 client ID and redirect URLs.
- Upload new versions of the app.
- Choose the app version published to the marketplace (after the app has been initially published).
- View the app's marketplace listing.
- Edit the app's marketplace listing.
- Use any uploaded app version before it's published.
- Invite/remove collaborators at the same company with the following roles: code editor, listing editor, tester.
- Invite/remove collaborators at a different company with the following role: tester.
- Automatically get access to both the developer portal and the developer menu unless an account admin has disabled self-selecting into the developer role.*
Listing editor
- View the app's marketplace listing.
- Edit the app's marketplace listing.
- Use any uploaded app version before it's published.
- Invite/remove collaborators at the same company with the following role: listing editor.
- Automatically get access to the developer portal unless an account admin has disabled self-selecting into the developer role.*
Tester
- View the app's marketplace listing.
- Use any uploaded app version before it's published.
- Automatically get access to the developer portal unless an account admin has disabled self-selecting into the developer role.*
User not added as collaborator
- View the app's marketplace listing after the app has been approved to be in the marketplace.
- Install the app's published version after the app has been approved to be in the marketplace.
What can application collaborator roles do?
Application collaborator roles are most useful for applying the principle of least privilege. This principle is that a user should only be given the minimum privileges necessary to perform their required tasks, minimizing the potential for unauthorized access or unintended actions.
For example, these roles can allow quality assurance testing by giving users the "Tester" role. Or, an admin can give marketing personnel limited access to edit the marketplace listing by assigning the "Listing editor" role.
Note that users on different Lucid accounts can be assigned the "Tester" role by anyone with "App owner" or "Code editor" roles.
How do I assign application collaborator roles?
Note that if you already created an application, skip to step 2.
Step 1: Go to your application on the developer portal
- Navigate to the developer portal.
- Open your application.
Step 2: Invite collaborators
- Click on the newly created app to open the app settings page.
- On the app settings page, click the "Invite" button.
- Input the email of the user you want to invite and the role they will be assigned. Note that this needs to be the user's email that is associated with their Lucid account. Also, the "Tester" role can be assigned to users on your account or users on different accounts.
Step 3: Remove collaborators
- If necessary, click the trash can icon next to any collaborators you want to remove from your app.
What are the limitations of application collaborator roles?
There are some instances when a user who has been sent a collaborator invitation is unable to accept it. This happens when the following are true:
- The admin control for self-selecting into the developer role is turned off on the account of the receiving user.
- The receiving user doesn't have the developer role assigned manually by the admin.
However, if the admin control for self-selecting into the developer role is turned on, accepting the collaborator invitation will automatically give the user access to developer tools, even if they didn't previously have access.
Admin controls
Lucid allows admins on Lucid Enterprise accounts to control which users can develop on Lucid’s APIs.
For Lucid’s REST and Extension APIs, admins can permit or restrict development by managing their users’ access to developer tools. With the instructions detailed below, admins can configure their account so:
- Any user on the account can develop on Lucid’s APIs.
- No users can develop.
- Only admin-designated users can develop.
For Lucid’s SCIM API, admins permit or restrict use or development by managing access to their account’s SCIM bearer token. Please note that use of Lucid’s SCIM API is limited to Lucid Enterprise accounts.
Developer tools
Lucid provides two developer tools that allow Lucid users to create, test, and distribute apps built on Lucid’s REST and Extension APIs:
- The developer portal, which allows developers to create new apps and OAuth 2.0 credentials needed to hit Lucid’s REST APIs, and to distribute apps.
- The developer menu, which allows developers to test apps built on Lucid’s Extension API.
Users without these tools are unable to build or distribute code with these APIs.
Managing the SCIM API
A SCIM bearer token is needed for an app to use Lucid’s SCIM API. This token can only be obtained in the admin panel, it is not available via the developer portal or developer menu. Only account admins can access the bearer token for an account.
Managing the REST and Extension APIs
Lucid admins can control which users are able to build on Lucid’s REST and Extension APIs by managing which users have access to developer tools. Note that users do not need developer tools to connect to or use apps published in the Lucid marketplace.
Admins can configure their account in any one of the following ways:
- Any user on the account can develop on Lucid’s APIs.
- No user can develop on Lucid’s APIs.
- Only admin-designated users can develop on Lucid’s APIs.
Admins can configure this by enabling or disabling developer self-select in the “User feature controls” section of the admin panel. When self-select is disabled, admins can assign individual users developer permissions in the “Users” section of the admin panel.
The default configuration allows all users to develop.
All users can develop
An admin can configure their account to allow any of their users to develop on Lucid’s REST and Extension APIs. Admins can do so by going to the “User feature controls” section of the admin panel, finding the “Developer controls” section, enabling the toggle, and pressing “Save changes.”
When the “Developer controls” toggle is enabled, users will be able to unlock developer features via their user settings. Please note that admins will be unable to see which users have unlocked developer features.
No users can develop
If an admin desires that no users are able to build apps on Lucid’s REST and Extension APIs, they need to 1) disable the “Developer controls” toggle in the admin panel, and 2) unassign the developer role from any user previously assigned it.
Admin-designated users can develop
Admins are able to assign specific users the ability to build apps on Lucid’s REST and Extension APIs by assigning a user the developer role. Assigning a user the developer role unlocks developer tools for that user, even if they haven’t enabled developer features in their user settings. The developer role can be assigned to a user in the “Users" section of the admin panel.
Also note that for security purposes, Lucid does not allow admins to assign themselves roles. This means admins are unable to assign themselves the developer role.
OAuth 2.0 Client Creation
In order to use any of the Lucid REST APIs, an app must have permission from the user to access their data. This permission can be granted with an OAuth 2.0 access token. Lucid allows developers to create an OAuth 2.0 client attached to their app for this purpose. This guide focuses on creating a new app in the developer portal along with its OAuth 2.0 client.
Goal
You’ll learn how to create an OAuth 2.0 client through the developer portal. This will include:
- Creating a new application
- Creating an OAuth 2.0 Client
Prerequisites
- A licensed Lucid account
- Access to developer tools
Glossary
Term
|
Definition |
---|---|
OAuth 2.0 | OAuth 2.0 is an industry-standard authorization framework that enables third-party applications to access a user's protected resources on a server without exposing their credentials. It allows users to grant limited access to their data while maintaining control over their sensitive information. |
Client ID | A client ID is a unique identifier assigned to a client application by the authorization server. It is used to authenticate and authorize the client application when making requests to the server, helping establish the client's identity and permissions. |
Client Secret | A client secret is a confidential and securely stored string or passphrase that is assigned to a client application by the authorization server. It serves as a form of authentication for the client application when interacting with the server. The client secret is used to verify the identity of the client application and protect against unauthorized access. |
Redirect URI | The redirect URI in OAuth 2.0 is a specific endpoint or URL that the authorization server uses to redirect the user's browser back to the client application after authentication and authorization. It serves as a callback mechanism to deliver the authorization code or access token to the client application. The redirect URI helps complete the OAuth 2.0 flow and allows the client application to obtain the necessary authorization credentials. |
Postman | Postman is a popular API development and testing tool that allows developers to easily send HTTP requests, observe responses, and analyze API behavior. |
Step by Step Walkthrough
Step 1: Create a new application on the developer portal
- Navigate to the developer portal.
- Click the “New Application” button.
- Input a name for your application.
- Note that this name can be changed in the app settings page later if necessary.
Step 2: Create an OAuth 2.0 client
- Click on the newly created app to access its settings.
- Navigate to the “OAuth 2.0” tab.
- Input a name for your OAuth 2.0 client.
- Note that this name will be publicly visible when users grant access to your application, and it cannot be changed.
Step 3: Note the client ID and client secret
- The client ID and client secret will be used to authenticate your application when users grant access to your application.
- If the client secret ever becomes compromised and needs to be reset, click the “Reset Client Secret” button on this page.
- Note that once the client secret is reset, it cannot be undone. Your app will immediately lose access to Lucid until the client secret is updated in your app as well.
Step 4: Register a Redirect URI
- Click the “Add Redirect URI” button on the OAuth 2.0 settings page.
- Input the URL that you want to redirect users back to once they have granted permissions to your app.
- This should be a URL that the app controls. Lucid will append the authorization code to the URL in the code query parameter.
- If using an API tool like Postman, you can use the Test Redirect URI to retrieve the code manually while developing your integration.
- For more more information, reference the glossary.
Limitations
When accessing Lucid APIs using OAuth 2.0, the following limitations apply:
- If the app is not published, only specified app collaborators can use the app with your client ID and secret. If you would like to develop an app that all Lucid users can use, refer to this publishing documentation.
- Authorization must happen in the browser, because a user must give consent for the client to access their information. For this reason, the authorization request cannot be made from most http clients.
Other Resources
- Additional OAuth 2.0 documentation
- Details of the OAuth 2.0 authorization process can be found at https://oauth.net/2/
Publishing
What is "Publishing"?
Publishing your app makes it easy for Lucid users to find and connect to your app. Lucid provides two publishing options:
- You can request your application be published in Lucid's Marketplace. Doing so provides nearly every Lucid user the option to “connect” to and use your app. Please note Lucid will review your app before it's published to the Marketplace.
- [Coming soon] A Lucid admin is able to publish an app internally. This means any user on your Lucid account can connect to the app.
For apps built on our Extension API, you will be able to publish specific versions of the extension package. Like a new app, each version of the extension package will be reviewed by Lucid before being released to the marketplace. When the published version changes, users connected to the app are automatically transitioned to the published version.
If you'd like to distribute your app to only select Lucid users, consider adding them to the app as a collaborator with the Tester role.
Publish to Lucid Marketplace
This section describes how to publish your app to the Lucid marketplace. At a high-level, the process for publishing an app is:
- Ensure your app meets all publishing requirements.
- Submit the app for Lucid review.
- Lucid will review the app.
- If/when the app passes the review, Lucid will release it to the marketplace.
Requirements
Before requesting your app to be published on the Lucid marketplace:
- Your app must have an extension package (i.e. be built on our Extension API), an OAuth 2.0 client (i.e. be built on our REST API), or both. Please see API-specific Requirements for additional requirements specific to which API your app uses.
- Your app must have a completed marketplace listing page. This page allows you to explain to potential users the basics of your app (what it does, where to go for support, why your app is incredible, etc.).
- You must create a ticket in Zendesk using this form (record the Zendesk ticket ID as you will need to provide it during submission). This ticket provides Lucid with information needed to review your app. Lucid will also use Zendesk to ask you any questions that arise during the app review.
Completing the marketplace listing
To complete the Marketplace listing, go to Marketplace listing in the application general section.
In the listing you will be able to provide general information about your application, images, support information, and more.
Applications with only an OAuth 2.0 client must provide a link (or multiple links) redirecting users to the webpage where they can connect to/authorize the app. You can either provide a single link, or one link per Lucid product supported by your application. If your OAuth 2.0-based app supports Lucid multiple products, it is important that the links are entered properly.
Once you have completed the listing, you will be able to see a preview of it and make sure everything is perfect.
Submitting for Review
A user must have the App owner collaborator role to request an app be published for the first time (see Application Collaborator Roles). If the application has been previously published, users with the Code editor role can also request updates to the app (e.g. new package versions or adding an OAuth 2.0 client) be published. Other collaborators won't be able to see the UI for publishing.
You submit an app/update for approval in the Developer Portal, specifically using the Publish application UI in the Distribution section of the app's page. Here you can also publish a previously approved extension package version (this automatically reverts all users connected to the app to that version).
The steps to publish an app will be slightly different depending on the app's components.
- If you have a package version, you will get to choose which version you want to publish.
- If you have an OAuth 2.0 client, you will see the name of the client that will be published.
- If you have both a package version and an OAuth 2.0 Client, both will be published at the same time.
- Important: After you publish an application with only an OAuth 2.0 client, you will be unable to add extension packages to the app in the future.
To submit an app with an extension package for review, you must select the package version (if there is an eligible one) that will be published. You will be able to select any package version that has not been revoked, currently published, or under review.
You can publish a previously approved package version at any time, which will trigger an automatic transition to that version for all connected users. Lucid provides this option in case there are unexpected issues with a new version and you need to quickly revert to a stable version.
If your application has an unpublished OAuth 2.0 client, it will be automatically included in the review process.
The action button will be disabled if certain publish requirements have not yet been met. You can hover your mouse over the button to see which requirements have not been met.
Clicking the action button will trigger either the app review process or publishing if no approval is needed. If the Lucid review process is required, you will be prompted to enter the Zendesk ticket ID discussed here. The link to create the Zendesk form will also be present on the modal.
Refer to the Application publication status to see the updated publication information of your app.
Checking your application status
The Publication status section will display information on which users can access your application, if a package version or OAuth 2.0 client is publicly available, and any pending review process.
Every collaborator on the application will be able to see the publication status.
Once the application has been reviewed and approved by our team, it will be automatically published on Lucid's marketplace. If you are publishing a new extension package version, every user with the current previous published version will automatically update to the new version.
Publish Internally
Coming soon!
API-specific Requirements
To publish an app (either internally or to the Lucid Marketplace), the app must meet requirements specific to the API it uses. Apps using both APIs must meet both sets of requirements.
REST API
These requirements apply if your application uses our REST API (e.g. has an OAuth 2.0 Client). Refer to the OAuth 2.0 Client Creation Walkthrough for steps on how to create an OAuth 2.0 Client. Please note that applications built on our legacy REST APIs can not be published.
Requirements:
- The name of your OAuth 2.0 client must accurately represent your application. This name will be shown to users when Lucid asks them whether they authorize your app to interact with their data.
- Verify you have registered all redirect URIs your application will use.
Extension API
These requirements apply if your application uses our Extension API (e.g. has an extension package). Go to your application in the Lucid Developer Portal and click on the
You can click on a package version to see more information about the version, as well as the “Install for me”, “Install for my account”, and “Revoke” options.
In order to publish your application with a package extension, you must have previously created a package version. If you haven't created one, follow the steps in Upload your extension package to do it. Once you are done, you will see a new entry on the table.
Uploading an Extension Package
Clicking on your application 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 package.zip file which is ready for upload to the Lucid Developer Portal. Go to your application, and click on the “Packages” tab. For more information look at Bundle your package for upload.
Then, click on the “+ New Version” button, and finally upload and submit the “package.zip” file generated in the previous step.
A new package version will be created, and will be ready to be published. You can follow this process anytime you need to upload new code for your extension package. Remember to update the
Custom shape libraries
What is a custom shape library?
Lucidchart and Lucidspark both support custom, user-created shape libraries. Custom shape libraries appear in the left panel of Lucid documents alongside Lucid's built-in shape libraries. Users can drag shapes from any custom shape libraries they have installed onto the canvas as they’re creating their documents.
To create a shape library, you can use the Extension API to create a shape library extension that can be published and shared through Lucid’s extension marketplace.
What can a custom shape library do?
Lucid's custom shape library framework allows you to create all sorts of powerful shapes. The following are features of custom shapes you can leverage.
Conditional formatting

Grouping and nesting

Images

Data linking

Anchor points

Repeat geometries

By combining these features, you can create intelligent shapes that bring clarity to your visualizations.
What are the limitations of custom shape libraries?
- User-defined international/i18n translations are parsed and verified but aren't currently used.
- Formatting inside of text areas is not implemented (text areas as a whole can be formatted, but the format can't change within the text area).
Examples of custom shape libraries:
Get started building a custom shape library:
If you're interested in building your own custom shape library, you can follow our comprehensive walkthrough.
Link Unfurling
What is link unfurling?
Link unfurling enables you to enrich the Lucid canvas by converting static text links into dynamic shapes. These shapes can include image thumbnails and titles, providing a more visually appealing and informative display for external users. By utilizing link unfurling, you can easily make content more captivating and interactive.
A link unfurling integration has two primary elements: its block and its control options.
Link unfurl block: Generated from an external URL, this block shows a thumbnail image and a page title, both sourced from the URL, alongside the data source's logo and name. Additionally, a text area at the bottom of the block allows users to enter detailed descriptions.
Link unfurl control options: Control options for the internal embed extension vary by Lucid product. In Lucidchart, the options are located in the context menu on the right-hand side of the canvas. In Lucidspark, they can be found at the top of the shape.
Control options vary depending on the type of content being embedded.
These options are available for all internal embeds:
The ability to copy the source link in the embedded thumbnail
The ability to open the embedded link in a new tab
The ability to refresh and resync with the data source
These options are available if there is at least one thumbnail image available:
The ability to toggle and choose an image to display on the thumbnail (when multiple thumbnail images are available)
The ability to extract one or more thumbnail images to use as shapes on the canvas
This option is available if the iframe URL is provided by your extension:
The ability to expand the embedded link within the document or board for viewing and editing, using an iframe URL provided by your extension
What are the limitations of Link Unfurling?
- Once thumbnail images have been extracted, it is not possible to resync them with the data source for the image shapes. However, a potential workaround is to re-extract the image after resyncing with the data source in the internal embed shape.
- Bidirectional data sync is not supported, meaning that while data can be imported into Lucid, updates to titles or images cannot be pushed back to the data source. A possible workaround for this issue is to use an iframe editor.
- Doesn't support in-editor video players. Video links unfurled need to be opened in a new window/tab.
Examples of Link Unfurling implementations
- Figma
- Google Docs
- Google Drive
- Google Sheets
- Google Slides
- Microsoft Excel
- Microsoft PowerPoint
- Microsoft Word
See more:
How to build internal embed integration
Data import and sync
What is data import and sync?
This function enables developers to take data from an external data source and pull it into Lucid. This can be done by:
- Hitting a third-party API and manually retrieving data.
- Setting up data connectors, which automatically pull data from third-party APIs.
What can data import and sync do?
- It can automatically pull data from external data sources to update properties of any Lucid element that can be data linked (shapes, lines, groups, or pages). Such properties include text, color, and more.
- Any changes made to the data in Lucid will reflect back in the external data source. Pushing updates back to the external source can be made automatic or designed to appear to the user in stages or batches.
How does data import and sync work?
Data import and sync relies on Lucid's Extension API, and uses:
- Editor Extensions to manage how data appears and acts on Lucid documents.
- (Optionally) Data Connectors to automatically fetch changes made in third party systems, and to automatically push changes made in Lucid back to third party systems.
- (Optionally) Shape Libraries to create custom shapes for displaying data.
You can step through the entire process of building a data import and sync cards integration here.
What are the limitations of data import and sync?
- Data imported via extensions does not currently work with Lucid's out-of-the-box solution for org charts or timelines. Nevertheless, it's possible for the extension to manually construct such objects.
- It's easy to work with data that can be represented tabularly. However, data formats that are not efficiently represented tabularly will be more difficult to work with due the the way Lucid stores data on the document.
Examples of data import and sync implementations
- Lucid Cards for Asana
- Lucid Cards for Google Sheets
- Step-by-step guide on how to build a data import and sync integration
Using web frameworks
When developing with the Extension API, you can utilize web frameworks to display content onto a panel or modal. The following sections will go over using Angular and React, but other frameworks can be used as well.
Angular
Complex custom UI built directly with HTML and Javascript can be difficult to write and maintain. There are a number of popular frameworks you can use to make UI development more scalable. Angular is one popular option. This section will walk you through building a simple Angular application that is displayed in a custom panel.
Step 1: Create the editor extension
Start by creating a new empty extension package, and adding an editor extension to that package:
npx lucid-package create ngtest
cd ngtest
npx lucid-package create-editor-extension with-cool-ui
Step 2: Create the Angular app and start the dev server on it
Now you will use the Angular CLI to create a new Angular application called
npm install @angular/cli webpack-shell-plugin-next
mkdir -p public/rightpanel
cd editorextensions/with-cool-ui
npx ng new rightpanel
cd rightpanel
npx ng serve # Leave this running while developing
Step 3: Replace webpack.config.js for the editor extension
Here you will use
const path = require('path');
const WebpackShellPluginNext = require('webpack-shell-plugin-next');
const angularTargets = [{name: 'rightpanel', port: 4200}];
module.exports = {
entry: './src/extension.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /[\\\/]resources[\\\/]/,
use: 'raw-loader',
exclude: /\.json$/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'bin/extension.js',
path: __dirname,
},
plugins: [
new WebpackShellPluginNext({
//When doing a watch build, run "ng serve" and update the html file to prefix http://localhost:4200/ to all the resource URLs
onWatchRun: {
scripts: angularTargets.map(
(target) =>
`mkdir -p ../../public/${target.name} &&` +
`curl http://localhost:${target.port} | ` +
`sed -E "s/(src|href)=\\"/\\\\1=\\"http:\\/\\/localhost:${target.port}\\//gi" > ` +
`../../public/${target.name}/index.html`,
),
blocking: true,
},
//When doing a full build, run "ng build" and then copy all the assets to the root level public folder
onBeforeNormalRun: {
scripts: angularTargets.map(
(target) =>
`mkdir -p ../../public/${target.name} &&` +
`cd ${target.name} && ` +
// `npx ng build` usually works, but this is more reliable when used with build tools such as bazel
`./node_modules/.bin/ng build && ` +
`cp -r dist/${target.name}/* ../../../public/${target.name}`
),
blocking: true,
swallowError: false,
safe: true,
},
}),
],
mode: 'development',
};
Step 4: Use the Angular app in a panel
Update
import {EditorClient, Panel, PanelLocation, Viewport} from 'lucid-extension-sdk';
const client = new EditorClient();
export class RightPanel extends Panel {
private static icon = 'https://lucid.app/favicon.ico';
constructor(client: EditorClient) {
super(client, {
title: 'From Angular',
url: 'rightpanel/index.html',
location: PanelLocation.RightDock,
iconUrl: RightPanel.icon,
});
}
}
const rightPanel = new RightPanel(client);
Step 5: Run the Lucid dev server
Make sure your
npx lucid-package test-editor-extension with-cool-ui
Step 6: Write your Angular app
With both the
To use static assets in your Angular app, you will need to place your static assets in package root level's public folder under
If you want to share classes or other code from your extension to your UI, then just add the following in
{
// ...
"compilerOptions": {
// ...
"paths": {
"@extension/*": ["../src/*"]
},
}
}
Then you will be able to import, for example, from
import {SharedThing, SharedClass} from '@extension/sharedthing';
Remember, of course, that just because you're sharing code doesn't mean you're in a shared runtime. You still have to send serializable messages back and from from your UI project like this. You could easily make a simple Angular injectable that receives messages that you can use in your UI components:
import {Injectable} from '@angular/core';
()
export class DataFromExtension {
public ids: string[] = [];
constructor() {
//Listen for lists of selected item IDs
window.addEventListener('message', (event) => {
if (event.data['ids']) {
this.ids = event.data['ids'];
}
});
//Once ready to receive those messages, ask the extension to refresh data
parent.postMessage('refresh', '*');
}
}
You can add something like this to your Panel class to keep your UI updated any time the current selection changes:
export class RightPanel extends Panel {
private readonly viewport = new Viewport(this.client);
constructor(client: EditorClient) {
//...
this.viewport.hookSelection(() => this.sendStateToFrame());
}
private sendStateToFrame() {
this.sendMessage({
ids: this.viewport.getSelectedItems().map((i) => i.id),
});
}
//When the app is loaded, it will send a message asking for an update.
protected messageFromFrame(message: any) {
if (message === 'refresh') {
this.sendStateToFrame();
}
}
}
Step 7 (bonus): Add drag and drop functionality
You can add controls to your custom panels that allow users to drag and drop a new block from your panel onto the canvas.
Use Viewport.startDraggingNewBlock from the panel
In the
You will be sending a few possible messages:
-
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 extension to the panel's Angular app:
The code of the
export class RightPanel extends Panel {
// ...
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': 'Custom Text'},
});
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.message == 'cancelDrag') {
this.viewport.cancelDraggingNewBlock();
}
}
}
You can see that
Here, you are just creating a new standard block type, but this operation works just as well with custom shapes from your shape libraries, like this:
export class RightPanel 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.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.message == 'cancelDrag') {
this.viewport.cancelDraggingNewBlock();
}
}
}
Write the Angular component
Writing a well-behaved drag and drop source requires some care. This example has all of 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.
Here is some sample code performing all of these operations with a simple div as the dragged element:
<div
class="drag"
(pointerdown)="pointerDown($event)"
>
Drag me
</div>
div.drag {
width: 100px;
height: 100px;
border: 4px solid red;
}
import {Component} from '@angular/core';
({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less'],
})
export class AppComponent {
private pointerDownEvent: PointerEvent | undefined;
//As of the last pointer event, is the (captured) pointer outside the iframe's bounds?
private pointerIsOut = false;
private documentPointerUp = (e: PointerEvent) => {
if(this.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();
};
private isInsideFrame(e: PointerEvent) {
const x = e.pageX - window.scrollX;
const y = e.pageY - window.scrollY;
return x >= 0 && x <= window.innerWidth && y >= 0 && y <= window.innerHeight;
}
private documentPointerMove = (e: PointerEvent) => {
const isInside = this.isInsideFrame(e);
if(!this.pointerIsOut && !isInside) {
this.startCanvasDrag();
} else if(this.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.
this.stopCanvasDrag();
}
this.pointerIsOut = !isInside;
//While dragging the HTML element around, move it around
//with relative positioning to keep it attached to the pointer cursor.
const target = this.pointerDownEvent?.target;
if (this.pointerDownEvent && target instanceof HTMLElement) {
target.style.position = 'relative';
target.style.top = e.pageY - this.pointerDownEvent.pageY + 'px';
target.style.left = e.pageX - this.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 (e.buttons == 0) {
this.stopDrag();
}
}
else {
//Notify the editor that it needs to simulate canvas pointer events
parent.postMessage({message:'pointermove', x:e.pageX - window.scrollX, y:e.pageY - window.scrollY}, '*');
}
};
//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.
private windowMessage = (e) => {
if (e.data === 'dragDone') {
stopDrag();
}
};
private startCanvasDrag() {
parent.postMessage({message: 'drag'}, '*');
window.addEventListener('message', this.windowMessage);
}
private stopCanvasDrag() {
window.removeEventListener('message', this.windowMessage);
parent.postMessage({message:'cancelDrag'}, '*');
}
//Start listening for pointer events on this iframe to implement drag & drop.
private startDrag() {
window.document.addEventListener('pointerup', this.documentPointerUp);
window.document.addEventListener('pointermove', this.documentPointerMove);
}
//Cancel drag & drop, and reset the DOM back to how it began.
private stopDrag() {
const target = this.pointerDownEvent?.target;
if (this.pointerDownEvent && target instanceof HTMLElement) {
target.style.position = 'static';
target.style.top = '';
target.style.left = '';
this.pointerDownEvent = undefined;
}
window.document.removeEventListener('pointerup', this.documentPointerUp);
window.document.removeEventListener('pointermove', this.documentPointerMove);
this.stopCanvasDrag();
}
public pointerDown(e: PointerEvent) {
//Store the event that started the drag, as a coordinate anchor.
this.pointerDownEvent = e;
this.pointerIsOut = false;
this.startDrag();
}
}
Using React
Similarly, you can use React to make custom UIs. This section will walk you through building a simple React application that is displayed in a custom panel.
Step 1: Create the editor extension
Start by creating a new empty extension package, and adding an editor extension to that package:
npx lucid-package create reacttest
cd reacttest
npx lucid-package create-editor-extension with-cool-ui
Step 2: Create the React app and start the dev server on it
Now you will use Create React App to bootstrap a new React application called
npm install webpack-shell-plugin-next
mkdir -p public/rightpanel
cd editorextensions/with-cool-ui/
npx create-react-app rightpanel --template typescript
cd rightpanel
npm start # Leave this running while developing
Step 3: Replace webpack.config.js for the editor extension
Here you will use
const path = require('path');
const WebpackShellPluginNext = require('webpack-shell-plugin-next');
const reactTargets = [{name: 'rightpanel', port: 3000}];
module.exports = {
entry: './src/extension.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /[\\\/]resources[\\\/]/,
use: 'raw-loader',
exclude: /\.json$/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'bin/extension.js',
path: __dirname,
},
plugins: [
new WebpackShellPluginNext({
//When doing a watch build, run "npm start" and update the html file to prefix http://localhost:3000/ to all the resource URLs
onWatchRun: {
scripts: reactTargets.map(
(target) =>
`mkdir -p ../../public/${target.name} &&` +
`curl http://localhost:${target.port} | ` +
`sed -E "s/(src|href)=\\"/\\\\1=\\"http:\\/\\/localhost:${target.port}\/gi" > ` +
`../../public/${target.name}/index.html`,
),
blocking: true,
},
// When doing a full build, run "npm run build" and then copy all the assets to the root level public folder
onBeforeNormalRun: {
scripts: reactTargets.map(
(target) =>
`mkdir -p ../../public/${target.name} &&` +
`cd ${target.name} && ` +
`npm run build && ` +
`sed -i -E "s/(src|href)=\\"\\//\\1=\\"\/gi" build/index.html &&` +
`cp -r build/* ../../../public/${target.name}`
),
blocking: true,
},
}),
],
mode: 'development',
};
Step 4: Use the React app in a panel
Update
import {EditorClient, Panel, PanelLocation, Viewport} from 'lucid-extension-sdk';
const client = new EditorClient();
export class RightPanel extends Panel {
private static icon = 'https://lucid.app/favicon.ico';
constructor(client: EditorClient) {
super(client, {
title: 'From React',
url: 'rightpanel/index.html',
location: PanelLocation.RightDock,
iconUrl: RightPanel.icon,
});
}
}
const rightPanel = new RightPanel(client);
Step 5: Run the Lucid dev server
Make sure your
npx lucid-package test-editor-extension with-cool-ui
Step 6: Write your React app
With both the
You might observe that the static assets generated by Create React App are not being loaded properly. To fix this issue, all you need to do is to place your static assets in package root level's public folder under
In order to share classes or other code between your extension and UI, you will need to install either craco or react-app-rewired. These tools allow you to override the default webpack settings used by Create React App:
cd rightpanel
npm install @craco/craco
Then create a file called
const path = require('path');
module.exports = {
webpack: {
alias: {
'@extension': path.resolve(__dirname, '../src'),
},
configure: webpackConfig => {
const scopePluginIndex = webpackConfig.resolve.plugins.findIndex(
({ constructor }) => constructor && constructor.name === 'ModuleScopePlugin'
);
webpackConfig.resolve.plugins.splice(scopePluginIndex, 1);
return webpackConfig;
}
},
};
Then create a file called
{
"compilerOptions": {
"paths": {
"@extension/*": ["../src/*"]
}
}
}
In
{
// ...
"extends": "./tsconfig.paths.json",
}
Then you should change all
{
// ...
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject"
}
}
Then you will be able to import, for example, from
import {SharedThing, SharedClass} from '@extension/sharedthing';
Remember, of course, that just because you're sharing code doesn't mean you're in a shared runtime. You still have to send serializable messages back and from from your UI project like this. You could easily add a eventListener in the
import React, { useEffect, useState } from 'react';
import './App.css';
function App() {
const [ids, setIds] = useState([]);
const handleMessage = (event: MessageEvent<any>) => {
if (event.data['ids']) {
setIds(event.data['ids']);
}
}
useEffect(() => {
window.addEventListener('message', handleMessage);
//Once ready to receive those messages, ask the extension to refresh data
window.parent.postMessage('refresh', '*');
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);
return (
<div className="App">
<div>selected Ids: {ids}</div>
</div>
);
}
export default App;
You can add something like this to your Panel class to keep your UI updated any time the current selection changes:
export class RightPanel extends Panel {
private readonly viewport = new Viewport(this.client);
constructor(client: EditorClient) {
//...
this.viewport.hookSelection(() => this.sendStateToFrame());
}
private sendStateToFrame() {
this.sendMessage({
ids: this.viewport.getSelectedItems().map((i) => i.id),
});
}
//When the app is loaded, it will send a message asking for an update.
protected messageFromFrame(message: any) {
if (message === 'refresh') {
this.sendStateToFrame();
}
}
}
External embeds
What is an external embed?
An external embed enables you to add a Lucid document directly in your webpage. This view-only version can be configured to automatically and near-instantly update whenever changes are made to the original Lucid document. Users can also interact with the Lucid document by zooming in and out or panning through the document.
Embed types
Lucid's API offers two solutions for external embeds: token-based and cookie-based. With our token-based embed solution, anyone with access to your webpage can view the document, even if they don't have a Lucid account. With our cookie-based embed solution, a user is required to be a collaborator on the document in order to view the embed in your webpage.
Token-based
- Choose this solution when you want users on your webpage to see Lucid documents, even if they don't have access to the document in Lucid or even a Lucid account.
- The embedded document picker available for the token-based embed enables your users to determine which document to embed.
- This solution relies on the OAuth 2.0 authentication standard: Any user adding a Lucid document to your webpage will first be asked whether they give permission to your application to do so.
- As part of this solution, Lucid provides a document picker component. Your app can incorporate this component to enable users to select which Lucid document to embed.
- Developers can configure token-based embeds to either stay up-to-date with any changes made to the Lucid document or to always be a static snapshot reflecting what the document looked like at the time it was inserted.
Cookie-based
- Choose this solution if you're prioritizing a fast development cycle and if you're comfortable with the embed only being viewable by users who have permission to view the document in Lucid.
- This solution is designed for a user to copy the Lucid URL of a document and paste it into your web application. Your application would then call Lucid's API and display the embedded viewer. Please note that no document picker is available with our cookie-based embed.
- The user must be logged in to Lucid to view the embed in your webpage. This is because the embed utilizes browser cookies to authorize displaying the embedded document in your webpage.
- The user must have permission to view the Lucid document to view the document in your webpage. If a user's view permissions to the document is removed, the embed will no longer be visible on your webpage.
- This is the simplest type of embed to build, as it only requires you to hit one endpoint in an iframe. It's also the simplest to use as the user only needs to copy and paste the Lucid document's URL.
What are the limitations of external embeds?
- Lucid external embeds are view-only. Users are unable to edit the Lucid document directly in your webpage. Instead, Lucid embeds provide a button that users click to open the document in Lucid in a new tab and edit there, as long as they have edit permissions to the document.
- The document picker (part of the token-based solution) and cookie-based solution both require third-party cookies to be enabled in the browser. Some browsers disable these cookies by default. Information on enabling third-party cookies can be found here: Enabling third party cookies.
- Embedding a Team Space is not currently supported. Additionally, although Lucidspark breakout boards can be embedded, they are not selectable via the document picker.
- While the token-based embedded experience provides a document picker, there is no document picker available for cookie-based embeds.
- The token-based embedded experience requires your integration to manage the granting and storage of OAuth 2.0 tokens securely. Cookie-based embeds do not require any token management. When viewing a cookie-based embed, users who do not have access will see a login or request-access screen instead.
Examples of external embed implementations:
Technical documentation
Cookie-based
Step 1: Create an application
- Go to the developer dashboard
- Create a new application
- Create an Oauth2 client
- Register the authorized embedding domain. To try it out in Codepen, put
https://cdpn.io in the list. - Grab your client ID. You’ll need it in the next step!
Step 2: Embedding Lucid links in your app
If you have a Lucid document URL (such as from a customer pasting it) and you want to embed it in your app, you can use an iframe with the URL
<p>Paste a Lucid link to see the document below!</p>
<iframe
referrerpolicy="strict-origin"
style="width: 700px; height: 500px;">
</iframe>
const lucidIframe = document.querySelector('iframe');
// make sure you register cdpn.io in your dev portal for this demo to work
const clientId = 'YOUR_CLIENT_ID';
document.addEventListener('paste', (event) => {
const maybeUrl = event.clipboardData.getData('text/plain');
if (maybeUrl.startsWith('https://lucid.app/')) {
const iframeUrl = new URL('https://lucid.app/embeds/link');
iframeUrl.searchParams.set('document', maybeUrl);
iframeUrl.searchParams.set('clientId', clientId);
lucidIframe.src = iframeUrl.toString();
}
})
The embed will use the share settings on the link that was pasted.
Token-based
A step-by-step guide to use the token-based OAuth 2.0 external embed is coming soon! If you're looking to create an external embed that's available to everyone, sign up here to be a technology partner! We'd love to learn more about how you use Lucid and take our partnership to the next level.
For additional access or technical questions, please contact us in our Lucid developer forum.
Lucid Cards Overview
What are Lucid Cards?
Lucid Cards provide a way to visually create and track tasks across the entire project workflow. You can assign stakeholders to work, set deadlines, and visualize how these tasks interact with each other.
Lucid Cards support rich, data-driven integrations for third-party task management systems. For example, you can import tasks from task management systems such as Airtable, Azure, Google Sheets, Monday.com, Jira, and more into Lucidspark as Lucid Cards to organize and plan tasks visually. Ideate and brainstorm with your team, then put those ideas into action by pushing tasks directly into your task management system of choice. Having this two-way sync feature ensures that changes are always reflected and both platforms are always up to date.
There are two ways to view the data in Lucid Cards. First, you can view them as shapes for a quick summary. Or, you can view more detailed versions for full context.
Lucid Cards shape: The shape of Lucid Cards provide a brief summary of task-related information. In your application, you have the flexibility to customize their content and arrangement.
Lucid Cards detail view: By clicking on the pencil icon, you can access the detailed view. In the displayed menu, you'll find all the relevant fields specific to the task. The available fields can be customized within your application.
Within the detail view for Lucid Cards, you can click the gear icon to bring up the Lucid Cards settings panel. This panel allows you to configure fields visible in the Lucid Cards detail view and to link or unlink the integration from the third party.
You can also unlink active accounts you've already connected.
How to create a data-synced card
There are four ways to create data-linked Lucid Cards.
Import from the left toolbar
In your application, you have the option to set up an import modal, which enables users to directly import data from third-party task management systems.
- Click the card integration icon in the left toolbar.
- Select the Import option in the callout.
- Authorize with the third-party task management system.
- In the import modal, search for the task to import.
- Click Import.
Create from the left toolbar
If you successfully configured two-way sync, you'll be able to create a new task from the left toolbar. This action will result in the creation of the task within the third-party task management system.
- Click the card integration icon in the left toolbar.
- Select the New task option in the callout.
- Authorize with the third-party task management system if you haven't already.
- Provide the necessary details to create a new task.
- Click Create.
Convert from regular Lucid Cards
If you successfully configured two-way sync, the following action will result in the creation of the task within the third-party task management system.
- Select the Lucid Cards icon on the left toolbar.
- Click anywhere on the board or drag the Lucid Cards shape out.
- Edit the card's content.
- Click on the card and click the wizard hat icon in the option bar.
- Choose the card integration to convert to.
- Follow the prompts.
- Click Create.
Convert an existing shape
If you successfully configured two-way sync, the following action will result in the creation of the task within the third-party task management system.
- Right click a shape.
- Select Convert to Card.
- Choose the card integration to convert to.
- Follow the prompts.
- Click Create.
What are the limitations of Lucid Cards?
- Currently, cards created as extensions don't work in Timelines.
- The custom fields that appear in the card details panel will be the same across the document.
- Card visualization is defined at build time, so users are unable to customize visualization.
Examples of Lucid Cards implementations
- Lucid Cards for Airtable
- Lucid Cards for Asana
- Lucid Cards for Azure DevOPs Cloud
- Lucid Cards for ClickUp
- Lucid Cards for Google Sheets
- Lucid Cards for Jira
- Lucid Cards for Monday.com
- Lucid Cards for Smartsheet
- Lucid Cards for Trello
Lucid Card Integrations
Introduction
Lucidspark allows rich data-driven integrations for task management systems. A Lucid card integration can:
- Import tasks from a task management system and add them as cards on a Lucidspark board.
- Create new cards on a Lucidspark board that sync back to the task management system as new tasks.
- Convert existing shapes, like sticky notes, to cards that sync back to the task management system as new tasks.
- Almost instantaneously sync changes made to data on Lucidspark cards back to the task management system.
- Almost instantaneously sync changes made in the task management system to any connected Lucidspark board.
Building a Lucid card integration requires the following steps:
- In an editor extension, extend LucidCardIntegration and implement the functionality you want to support.
- Define an OAuth provider in your package manifest to allow API calls to the data source.
- Build a publicly-routable data connector that can read and write from the data source, and include configuration for it in your package manifest.
This section of the developer guide will walk you through building a Lucid card integration to visualize Todoist tasks.
Goal
By the end of this guide you will have created a Lucid cards Todoist integration that allows you to import your Todoist tasks into Lucidspark.
Prerequisites
Make sure that you have the latest versions of lucid-package and lucid-extension-sdk npm modules installed, and you have access to the developer portal. Check out the Getting started section for details.
Step by Step Walkthrough
Create the application
Run the following commands to create a new application and set up an editor extension within that application:
npx lucid-package create todoist
cd todoist
npx lucid-package create-editor-extension todoist
Extend LucidCardIntegration
In the folder
import {LucidCardIntegration, EditorClient} from 'lucid-extension-sdk';
export class TodoistCardIntegration extends LucidCardIntegration {
constructor(private readonly editorClient: EditorClient) {
super(editorClient);
}
// You will fill this in soon
}
Now that you have created a new Lucid card integration, you need to construct and register it. Replace the code in
import {EditorClient, LucidCardIntegrationRegistry} from 'lucid-extension-sdk';
import {TodoistCardIntegration} from './todoistcardintegration';
const client = new EditorClient();
LucidCardIntegrationRegistry.addCardIntegration(client, new TodoistCardIntegration(client));
Initialize constants
A functional Lucid card integration depends on two individual components, the editor extension and the data connector, and it is essential to ensure proper communication between these two components. In particular, you will need to make sure that the schema used to represent the data in the extension and the data connector remain in sync.
Create a new folder called
export const DataConnectorName = 'todoist';
export const CollectionName = 'Tasks'; // You will use this later to store the data you get from Todoist
// You will add more constants here in the future
Basic configuration
Now you can begin configuring your Lucid card integration. These fields are required for every Lucid card integration:
-
label : the name of the integration, e.g. "Todoist". -
itemLabel : what to call one data item, e.g. "Task" or "Todoist task". -
itemsLabel : what to call multiple data items, e.g. "Tasks" or "Todoist tasks". -
iconUrl : a URL (a data URI is fine) pointing to an icon representing the integration. This will be displayed at up to 32x32 CSS pixels in size. -
dataConnectorName : the name of the data connector for this card integration. This will be specified in the package manifest.
We also allow you to optionally configure some more fields that let you customize your card integration:
-
textStyle : styles to be applied to all text on any cards created by this integration.
export class TodoistCardIntegration extends LucidCardIntegration {
// ...
public label = 'Todoist';
public itemLabel = 'Todoist task';
public itemsLabel = 'Todoist tasks';
public iconUrl = 'https://cdn-cashy-static-assets.lucidchart.com/extensibility/packages/todoist/Todoist-logo-for-card.svg';
public dataConnectorName: string = DataConnectorName; // Imported from names.ts that was created above
}
Field configuration
Many task management systems can have hundreds of fields, and it may be impractical or not useful to import all of them. To that end, card integrations allow the developer to configure which fields should be imported.
Define all the fields you want to import from Todoist in
// ...
export enum DefaultFieldNames {
Id = 'Id',
Content = 'Content',
Description = 'Description',
Completed = 'Completed',
Due = 'Due Date',
Link = 'Link',
Project = 'Project',
}
Every card integration must provide a fieldConfiguration object with the following members:
export class TodoistCardIntegration extends LucidCardIntegration {
// ...
public fieldConfiguration = {
getAllFields: (dataSource: DataSourceProxy) => {
return Promise.resolve([...Object.values(DefaultFieldNames)]);
},
onSelectedFieldsChange: async (dataSource: DataSourceProxy, selectedFields: string[]) => {},
};
}
Card shape configuration
Lucid card integrations must provide the default configuration to use for the appearance of cards imported or created by the integration.
As with the
The following information can be provided:
-
cardConfig.fieldNames : an array of field names that should be displayed as text fields on the card shapes on-canvas -
cardConfig.fieldStyles : optionally, text style overrides that should be applied for specific fields, on top of the defaulttextStyle described in Basic configuration. -
cardConfig.fieldDisplaySettings : information about fields that should be displayed as data graphics on card shapes -
cardDetailsPanelConfig.fields : an array of fields that has been imported and should be shown in the card details panel, with aname and optionally alocked boolean indicating whether the field cannot be unchecked from the list of fields to import
The following example configures the Todoist Lucid card to display the data from the content field on the Todoist task as text. Furthermore, the ID and due date of the task will be displayed as badges on the card. The content, description and due date of the task will also be shown in the card details panel:
export class TodoistCardIntegration extends LucidCardIntegration {
// ...
public getDefaultConfig = (dataSource: DataSourceProxy) => {
return Promise.resolve({
cardConfig: {
fieldNames: [DefaultFieldNames.Content],
fieldDisplaySettings: new Map([
[
DefaultFieldNames.Id,
{
stencilConfig: {
displayType: FieldDisplayType.SquareImageBadge,
valueFormula:
'="https://cdn-cashy-static-assets.lucidchart.com/extensibility/packages/todoist/Todoist-logo-for-card.svg"',
onClickHandlerKey: OnClickHandlerKeys.OpenBrowserWindow,
linkFormula: '=@Link',
horizontalPosition: HorizontalBadgePos.RIGHT,
tooltipFormula:
'=IF(ISNOTEMPTY(LASTSYNCTIME), "Last synced " & RELATIVETIMEFORMAT(LASTSYNCTIME), "Open in Todoist")',
backgroundColor: '#00000000',
},
},
],
[
DefaultFieldNames.Due,
{
stencilConfig: {
displayType: FieldDisplayType.DateBadge,
tooltipFormula: '=@"Due Date"',
},
},
],
]),
},
cardDetailsPanelConfig: {
fields: [
{
name: DefaultFieldNames.Content,
locked: true,
},
{
name: DefaultFieldNames.Description,
locked: true,
},
{
name: DefaultFieldNames.Due,
locked: true,
},
{
name: DefaultFieldNames.Completed,
locked: true,
}
],
},
});
};
}
Field display settings
For fields included in
In each case, only the
backgroundColor
For displayType values that support it, you may provide an override for the background color for this data. If this is not provided, the background color will default to the background color of the card itself, darkened 5%.
This may be provided as a literal color hex, e.g. "#00ff00ff" or as a formula by starting the string with "=", e.g. "=darken("#ffffff", 0.5)"
valueFormula
If specified, the result of this formula (executed in the context of the data item associated with this card) will be used instead of the raw field value when creating the data graphic. This can be useful if, for example, you want to convert an ID into a URL.
tooltipFormula
If specified, the result of this formula (executed in the context of the data item associated with this card) will be used as a tooltip when the user hovers the cursor over this data graphic.
horizontalPosition
Each display type has its default location on the card. You can override those default locations by setting these values.
verticalPosition
Each display type has its default location on the card. You can override those default locations by setting these values.
onClickHandlerKey
If specified, what behavior should happen when the user clicks on the data graphic generated via the above displayType?
linkFormula
If onClickHandlerKey is OpenBrowserWindow, this formula calculates the URL to open.
Create the import modal
Most Lucid card integrations will allow users to import data from their data source using the standard card import modal. To configure that modal for search and import, specify the
In the
import {
CollectionDefinition,
CollectionProxy,
EditorClient,
ExtensionCardFieldDefinition,
SerializedFieldType,
} from 'lucid-extension-sdk';
export class TodoistImportModal {
constructor(private readonly editorClient: EditorClient) {}
// Stub these out for now
public async getSearchFields(
searchSoFar: Map<string, SerializedFieldType>,
): Promise<ExtensionCardFieldDefinition[]> {
throw 'Not implemented';
}
public async search(fields: Map<string, SerializedFieldType>): Promise<{
data: CollectionDefinition;
fields: ExtensionCardFieldDefinition[];
partialImportMetadata: {collectionId: string; syncDataSourceId?: string};
}> {
throw 'Not implemented';
}
public async import(
primaryKeys: string[],
searchFields: Map<string, SerializedFieldType>,
): Promise<{collection: CollectionProxy; primaryKeys: string[]}> {
throw 'Not implemented';
}
}
Now, construct this class and assign it to the import field of the
export class TodoistCardIntegration extends LucidCardIntegration {
// ...
public importModal = new TodoistImportModal(this.editorClient);
}
Custom Import Modals
If you would rather utilize a custom UI to import data as cards to Lucid, you can utilize the
{
"primaryKeys": [
"pkey1",
"pkey2"
],
}
import {
CollectionDefinition,
CollectionProxy,
EditorClient,
ExtensionCardFieldDefinition,
SerializedFieldType,
} from 'lucid-extension-sdk';
export class TodoistImportModal {
constructor(private readonly editorClient: EditorClient) {}
srcUrl: String = "https://myCustomIframe.com";
width: number = 800;
height: number = 600;
public async import(primaryKeys: string[]): Promise<{collection: CollectionProxy; primaryKeys: string[]}> {
throw 'Not implemented';
}
}
Run the integration
This is a good time to check if everything is working as intended. In the root of you application, execute the following command to run your extension:
npx lucid-package test-editor-extension todoist
Now open a Lucidspark board and enable the
You should now see the Todoist integration show up in the submenu that is shown when you click on the more tools button (ellipses icon) located at the bottom of the left toolbar. Click the pin icon on the right to pin the Todoist integration to your toolbar.
You can now trigger the import modal. There's an error displayed because the search callback just throws an error.
Connecting to data
Now that you have the import modal showing up, you will need to fetch data from Todoist's OAuth 2.0 REST API to populate the import modal.
Configuring OAuth
The Lucid extensibility platform allows extensions to authorize with a third party using OAuth 2.0. To do so, you will first need to generate a unique identifier for your app.
Navigate to developer portal and create a new application. Once created, copy the app ID from the URL and paste it in the
Now fill in the OAuth 2.0 provider details in the
{
// ...
"oauthProviders": [
{
"name": "todoist",
"title": "Todoist",
"authorizationUrl": "https://todoist.com/oauth/authorize",
"tokenUrl": "https://todoist.com/oauth/access_token",
"scopes": ["data:read_write"],
"domainWhitelist": ["https://api.todoist.com", "https://todoist.com"],
"clientAuthentication": "clientParameters"
}
]
}
Create the OAuth client
Before you can start making API requests to Todoist, you will also need to set up an OAuth 2.0 client in Todoist. Navigate to the Todoist developer console and create a new app.
Once created, you will need to provide the OAuth redirect URL to Todoist. This is the URL users will be redirected to after granting your application access to their Todoist instance. Since Lucid handles OAuth 2.0 authentication for your app, a URL of the following form will need to be provided:
You will need to replace
Once you've entered the redirect URL in Todoist, click save settings.
Give your app the client credentials
Now that you have the client set up, you will need to give your app the appropriate credentials so that it can communicate with the Todoist client.
Create a new file
{
"clientId": "Client ID for your OAuth provider",
"clientSecret": "Client secret for your OAuth provider"
}
Create the Todoist client
With OAuth configured you can now begin making requests to Todoist to populate the import modal.
In
export interface Project {
id: string;
name: string;
comment_count: number;
}
export interface TodoistDate {
date: string;
is_recurring: boolean;
datetime: string;
string: string;
timezone: string;
}
export interface Task {
id: string;
is_completed: boolean;
content: string;
description: string;
due: TodoistDate;
url: string;
}
In
import {EditorClient, HumanReadableError, XHRResponse} from 'lucid-extension-sdk';
import { Project, Task } from '../model/todoistmodel';
export class TodoistClient {
constructor(private readonly client: EditorClient) {}
private readonly todoistOAuthProviderName = 'todoist';
private readonly baseUrl = 'https://api.todoist.com/rest/v2/'
public async getProjects(): Promise<Project[]> {
const rawResponse = await this.makeGetRequest(`${this.baseUrl}projects`);
return this.parseAsAny(rawResponse) as Project[];
}
public async getTasks(projectId: string, search?: string): Promise<Task[]> {
const filterParam = search ? encodeURI(`&filter=search:${search}`) : '';
const rawResponse = await this.makeGetRequest(`${this.baseUrl}tasks?project_id=${projectId}${filterParam}`);
return this.parseAsAny(rawResponse) as Task[];
}
private async makeGetRequest(url: string) {
try {
const response = await this.client.oauthXhr(this.todoistOAuthProviderName, {
url,
method: 'GET',
responseFormat: 'utf8',
});
return response;
} catch (error) {
console.log('Error:', error);
throw this.errorFromResponse(error);
}
}
private errorFromResponse(response: any) {
try {
return new HumanReadableError(this.parseAsAny(response).errors[0].message);
} catch (error) {
return new Error(JSON.stringify(response));
}
}
private parseAsAny(data: XHRResponse): any {
switch (data.responseFormat) {
case 'utf8':
return JSON.parse(data.responseText) as any;
case 'binary':
return JSON.parse(data.responseData as any) as any;
}
}
}
Finally, create an instance of this client class in
export class TodoistImportModal {
private todoistClient: TodoistClient;
constructor(private readonly editorClient: EditorClient) {
this.todoistClient = new TodoistClient(editorClient);
}
// ...
}
Populate the import modal
You can now begin implementing the callbacks in the
getSearchFields callback
The
The following example displays a simple text search box, plus a dropdown to select what project they would like to see tasks from:
export class TodoistImportModal {
// ...
private readonly searchField = 'search';
private readonly projectField = 'project';
public async getSearchFields(
searchSoFar: Map<string, SerializedFieldType>,
): Promise<ExtensionCardFieldDefinition[]> {
const projects = await this.todoistClient.getProjects();
const fields: ExtensionCardFieldDefinition[] = [
{
name: this.searchField,
label: 'Search',
type: ScalarFieldTypeEnum.STRING,
},
{
name: this.projectField,
label: 'Project',
type: ScalarFieldTypeEnum.STRING,
default: projects[0]?.id,
constraints: [{type: FieldConstraintType.REQUIRED}],
options: projects.map((project) => ({
label: project.name,
value: project.id,
})),
},
];
return fields;
}
// ...
}
Supported search field types
While this integration will only be using the text field and a single select dropdown during import, Lucid supports several other types of user input fields.
The following examples are all currently-supported search field types:
{
name: 'search',
label: 'Search',
type: ScalarFieldTypeEnum.STRING,
}
{
name: 'complete',
label: 'Status',
//Note: May be NUMBER or STRING as well, depending on the "value" set on options below
type: ScalarFieldTypeEnum.BOOLEAN,
constraints: [{type: FieldConstraintType.MAX_VALUE, value: 1}],
options: [
{label: 'Completed', value: true},
{label: 'Not Completed', value: false},
],
}
{
name: 'complete',
label: 'Status',
//Note: May be NUMBER or STRING as well, depending on the "value" set on options below
type: ScalarFieldTypeEnum.BOOLEAN,
options: [
{label: 'Completed', value: true},
{label: 'Not Completed', value: false},
],
}
const optionsCallback = LucidCardIntegrationRegistry.registerFieldOptionsCallback(client, async () => {
return [
{label: 'Completed', value: true},
{label: 'Not Completed', value: false},
];
});
// ...
{
name: 'complete',
label: 'Status',
type: ScalarFieldTypeEnum.BOOLEAN,
options: optionsCallback,
}
const searchCallback = LucidCardIntegrationRegistry.registerFieldSearchCallback(client, async (searchText) => {
return [
{label: 'Tabitha Ross', value:'Tabitha Ross'},
{label: 'Stanley Browning', value:'Stanley Browning'},
{label: 'Randall Lucas', value:'Randall Lucas'},
{label: 'Kira Ellis', value:'Kira Ellis'},
{label: 'Dale Bauer', value:'Dale Bauer'},
{label: 'Itzel Knight', value:'Itzel Knight'},
{label: 'Sage Beltran', value:'Sage Beltran'},
{label: 'Iris Ponce', value:'Iris Ponce'},
{label: 'Gisselle Conway', value:'Gisselle Conway'},
{label: 'Emely Williams', value:'Emely Williams'},
{label: 'Elena Arias', value:'Elena Arias'},
{label: 'Sarahi Aguirre', value:'Sarahi Aguirre'},
].filter(one => one.label.includes(searchText));
}),
// ...
{
name: 'owner',
label: 'Owner',
type: ScalarFieldTypeEnum.STRING,
search: searchCallback,
}
search callback
The next step is to implement the actual search functionality based on the values the user has entered into the search fields you specified above.
The search callback returns a
In the example below, requests are being made to Todoist that search tasks belonging to the specified project. It then uses the response to populate the import modal:
export class TodoistImportModal {
// ...
public async search(fields: Map<string, SerializedFieldType>): Promise<{
data: CollectionDefinition;
fields: ExtensionCardFieldDefinition[];
partialImportMetadata: {collectionId: string; syncDataSourceId?: string};
}> {
let search = fields.get(this.searchField);
if (!isString(search)) {
search = '';
}
const projectId = fields.get(this.projectField) as string | undefined;
const tasks = projectId ? await this.todoistClient.getTasks(projectId, search) : [];
return {
data: {
schema: {
fields: [
{
name: DefaultFieldNames.Id,
type: ScalarFieldTypeEnum.STRING,
mapping: [SemanticKind.Id],
},
{
name: DefaultFieldNames.Content,
type: ScalarFieldTypeEnum.STRING,
mapping: [SemanticKind.Title],
},
{
name: DefaultFieldNames.Completed,
type: ScalarFieldTypeEnum.BOOLEAN,
},
{
name: DefaultFieldNames.Due,
type: ScalarFieldTypeEnum.DATEONLY,
mapping: [SemanticKind.EndTime],
},
],
primaryKey: [DefaultFieldNames.Id],
},
items: new Map(
tasks.map((task) => [
JSON.stringify(task.id),
{
[DefaultFieldNames.Id]: task.id,
[DefaultFieldNames.Content]: task.content,
[DefaultFieldNames.Completed]: task.is_completed,
[DefaultFieldNames.Due]: task.due
? {ms: Date.parse(task.due.date), isDateOnly: true}
: undefined,
},
]),
),
},
fields: [
{
name: DefaultFieldNames.Content,
label: DefaultFieldNames.Content,
type: ScalarFieldTypeEnum.STRING,
},
{
name: DefaultFieldNames.Completed,
label: DefaultFieldNames.Completed,
type: ScalarFieldTypeEnum.BOOLEAN,
},
{
name: DefaultFieldNames.Due,
label: 'Due',
type: ScalarFieldTypeEnum.STRING,
},
],
partialImportMetadata: {
collectionId: CollectionName,
syncDataSourceId: projectId,
},
};
}
// ...
}
The optional
onSetup callback
During the import modal setup, if you want to add any custom setup code, you can do by implementing this optional callback.
It is going to be called everytime right after the modal is displayed and before the first call to
Run the integration
At this point you can run the integration to test whether your import modal is functioning properly. Make sure to add some tasks to your Todoist account!
Note: If you have the extension running locally you do not need to restart it when you make changes to the code. The local development server will refresh automatically when changes are detected. All you need to do is refresh the Lucidspark board to get the newest version of your extension.
Import data as Lucid cards
Now that you've allowed the user to find the tasks they want to import, you need to provide the callback that actually imports the selected tasks.
The import for this card integration will be performed with the help of a data-connector. A data-connector is run on its own server independent of the Lucid extension. The extension will make a request to the data connector to perform an import and pass in the required information. It is the data-connector's responsibility to fetch the data from the external source, convert it into a format Lucid understands and send a request to Lucid containing the data to be imported.
Note: To make the development experience easier, the Lucid sdk offers a debug server that can be used for local development. This guide will walk you through using the debug server. However, to make the integration publicly available, you will have to host the data-connector and provide a public URL to it in the manifest.
Create the data-connector
In the root of you application run the following command to create a new data-connector:
This will create a new directory named
To provide the data-connector details to the extension, in the
{
// ...
"dataConnectors": [
{
"name": "todoist",
"oauthProviderName": "todoist",
"callbackBaseUrl": "http://localhost:3001?kind=action&name=",
"dataActions": {
"Import": "Import"
}
}
]
}
This declares a data-connector named
Add the import data action name to
// ...
export enum DataAction {
Import = 'Import'
}
Todoist provides a typescript sdk to communicate with their API. To use this sdk in you data-connector, execute the following command in
Define the schema
You will need to convert the data you get from Todoist into a format compatible with Lucid. Begin by defining this Lucid compatible format.
In
import {declareSchema, FieldConstraintType, ItemType, ScalarFieldTypeEnum, SemanticKind, SerializedLucidDateObject} from 'lucid-extension-sdk';
import {DefaultFieldNames} from '../../../common/names';
import {Task as TodoistTask, TodoistApi as TodoistClient, DueDate as TodoistDueDate} from '@doist/todoist-api-typescript';
export const taskSchema = declareSchema({
primaryKey: [DefaultFieldNames.Id],
fields: {
[DefaultFieldNames.Id]: {
type: ScalarFieldTypeEnum.STRING,
mapping: [SemanticKind.Id],
constraints: [{type: FieldConstraintType.LOCKED}],
},
[DefaultFieldNames.Completed]: {
type: ScalarFieldTypeEnum.BOOLEAN,
mapping: [SemanticKind.Status],
},
[DefaultFieldNames.Content]: {
type: ScalarFieldTypeEnum.STRING,
mapping: [SemanticKind.Name],
},
[DefaultFieldNames.Description]: {
type: ScalarFieldTypeEnum.STRING,
mapping: [SemanticKind.Description],
},
[DefaultFieldNames.Due]: {
type: [ScalarFieldTypeEnum.DATE, ScalarFieldTypeEnum.DATEONLY, ScalarFieldTypeEnum.NULL] as const,
mapping: [SemanticKind.EndTime],
},
[DefaultFieldNames.Link]: {
type: ScalarFieldTypeEnum.STRING,
mapping: [SemanticKind.URL],
constraints: [{type: FieldConstraintType.LOCKED}],
},
[DefaultFieldNames.Project]: {
type: ScalarFieldTypeEnum.STRING
}
},
});
export type TaskFieldsStructure = typeof taskSchema.example;
export type TaskItemType = ItemType<TaskFieldsStructure>;
Also add helper functions that convert the data you get from Todoist into this format:
// ...
export function getFormattedTask(task: TodoistTask): TaskItemType {
return {
[DefaultFieldNames.Id]: task.id,
[DefaultFieldNames.Content]: task.content,
[DefaultFieldNames.Description]: task.description,
[DefaultFieldNames.Completed]: task.isCompleted,
[DefaultFieldNames.Due]: task.due ? getFormattedDueDate(task.due) : null,
[DefaultFieldNames.Link]: task.url,
[DefaultFieldNames.Project]: task.projectId,
};
}
function getFormattedDueDate(dueDate: TodoistDueDate): SerializedLucidDateObject | null {
const dateToUse = dueDate.datetime || dueDate.date;
if (dateToUse) {
return {
ms: Date.parse(dateToUse)
};
}
return null;
}
Create the import action
With the schema defined, you can now have the data-connector perform an import. In
import {TodoistApi as TodoistClient} from '@doist/todoist-api-typescript';
import {DataConnectorAsynchronousAction} from 'lucid-extension-sdk/dataconnector/actions/action';
import {CollectionName} from '../../../common/names';
import {getFormattedTask, taskSchema} from '../collections/taskcollections';
export const importAction: (action: DataConnectorAsynchronousAction) => Promise<{success: boolean}> = async (
action,
) => {
// action.data contains data passed to the data-connector by the extension when an action is invoked. In this integration,
// you will have the extension pass in the ID's of the tasks the user selected in the import modal.
const taskIds = action.data as string[];
const todoistClient = new TodoistClient(action.context.userCredential);
// Fetch the task data from Todoist
const fullTaskData = await todoistClient.getTasks({ids: taskIds});
// Convert the data into a Lucid compatible format
const formattedTaskData = fullTaskData.map(getFormattedTask);
// Send the imported data to Lucid
await action.client.update({
dataSourceName: 'Todoist',
collections: {
[CollectionName]: {
schema: {
fields: taskSchema.array,
primaryKey: taskSchema.primaryKey.elements,
},
patch: {
items: taskSchema.fromItems(formattedTaskData),
},
},
},
});
return {success: true};
};
Now, you can register the import action with the data-connector. Update
import {DataConnector, DataConnectorClient} from 'lucid-extension-sdk';
import {DataAction} from '../../common/names';
import {importAction} from './actions/import';
export const makeDataConnector = (client: DataConnectorClient) =>
new DataConnector(client).defineAsynchronousAction(DataAction.Import, importAction);
Run the debug server
In the
npx nodemon debug-server.ts
You should see the following output:
Routing / (Import)
Listening on port 3001
Call the import action
With the data-connector setup, you should now be able to update the
The
When the callback returns, the import should be complete, and the imported data should exist in a collection on the current document:
export class TodoistImportModal {
// ...
public async import(
primaryKeys: string[],
searchFields: Map<string, SerializedFieldType>,
): Promise<{
collection: CollectionProxy;
primaryKeys: string[];
}> {
const projectId = searchFields.get(this.projectField);
if (!isString(projectId) || !projectId) {
throw new Error('No project selected');
}
// Call the import action
await this.editorClient.performDataAction({
actionName: DataAction.Import,
syncDataSourceIdNonce: projectId,
dataConnectorName: DataConnectorName,
actionData: primaryKeys.map((pk) => JSON.parse(pk)), // pass in the ID's of the selected tasks to the data-connector
asynchronous: true,
});
// Wait for the import to complete
const collection = await this.editorClient.awaitDataImport(
DataConnectorName,
projectId,
CollectionName,
primaryKeys,
);
return {collection, primaryKeys};
}
}
Call import action with custom import modal
If you decided to utilize the custom import modal, then you will still need to define your import callback. The goal is the same as the method for the standard import method, with the exception that you don't have access to search terms, as your iframe should have already selected which cards you would like to import.
export class TodoistImportModal {
// ...
public async import(
primaryKeys: string[],
): Promise<{
collection: CollectionProxy;
primaryKeys: string[];
}> {
const projectId = this.getProjectId(primaryKeys); // This method would utilize similar logic to the getSerachFields method.
// Call the import action
await this.editorClient.performDataAction({
actionName: DataAction.Import,
syncDataSourceIdNonce: projectId,
dataConnectorName: DataConnectorName,
actionData: primaryKeys.map((pk) => JSON.parse(pk)), // pass in the ID's of the selected tasks to the data-connector
asynchronous: true,
});
// Wait for the import to complete
const collection = await this.editorClient.awaitDataImport(
DataConnectorName,
projectId,
CollectionName,
primaryKeys,
);
return {collection, primaryKeys};
}
}
Perform an import
With both the editor-extension and data-connector running, you should be able to import Todoist tasks as Lucid cards.
Fetching updates from Todoist
When linked to an external source, Lucid can update your card when changes are made outside of the document. This can be done via two different methods, hard refresh and polling, both of which are called outside of your extension when certain criteria are met.
Declaring the hard refresh and polling actions
Declare the hard refresh and polling actions by adding
{
//...
"dataConnectors": [
{
"name": "todoist",
"oauthProviderName": "todoist",
"callbackBaseUrl": "http://localhost:3001?kind=action&name=",
"dataActions": {
"Import": "Import",
"HardRefresh": "HardRefresh",
"Poll": "Poll"
}
}
]
}
Then add
...
export enum DataAction {
Import = 'Import',
HardRefresh = 'HardRefresh',
Poll = 'Poll'
}
Create the hard refresh action
A hard refresh allows your extension to refresh all of the data on a document when that document is opened for the first time after it has been closed by all users for more than 5 minutes. To add hard refresh support to your data connector, add a new file named
import {TodoistApi as TodoistClient} from '@doist/todoist-api-typescript';
import {DataConnectorAsynchronousAction} from 'lucid-extension-sdk';
import {isString} from 'lucid-extension-sdk/core/checks';
import {CollectionName} from '../../../common/names';
import {getFormattedTask, taskSchema} from '../collections/taskcollections';
export const hardRefreshAction: (action: DataConnectorAsynchronousAction) => Promise<{success: boolean}> = async (
action
) => {
const todoistClient = new TodoistClient(action.context.userCredential);
// Figure out which tasks you have already imported
let taskIds: string[] = [];
Object.keys(action.context.documentCollections).forEach((key) => {
if (key.includes('Tasks')) {
taskIds = taskIds.concat(
action.context.documentCollections?.[key].map((taskId) => JSON.parse(taskId)).filter(isString),
);
}
});
// If no tasks were imported, then you don't need to refresh
if (taskIds.length == 0) {
return {success: true};
}
// Update any tasks that you found
const fullTaskData = await todoistClient.getTasks({ids: taskIds});
const formattedTaskData = fullTaskData.map(getFormattedTask);
// Send the imported data to Lucid
await action.client.update({
dataSourceName: 'Todoist',
collections: {
[CollectionName]: {
schema: {
fields: taskSchema.array,
primaryKey: taskSchema.primaryKey.elements,
},
patch: {
items: taskSchema.fromItems(formattedTaskData),
},
},
},
});
return {success: true};
};
Now, you can register the hard refresh action with the data connector. Update
import {DataConnector, DataConnectorClient} from 'lucid-extension-sdk';
import {DataAction} from '../../common/names';
import {importAction} from './actions/import';
import {hardRefreshAction} from './actions/hardrefresh';
export const makeDataConnector = (client: DataConnectorClient) =>
new DataConnector(client).defineAsynchronousAction(DataAction.Import, importAction).defineAsynchronousAction(DataAction.HardRefresh, hardRefreshAction);
Create the poll action
The poll action allows your extension to refresh all of the data on a document periodically while the document is open. In many cases, the poll action can utilize the same code as the hard refresh action:
import {DataConnector, DataConnectorClient} from 'lucid-extension-sdk';
import {DataAction} from '../../common/names';
import {importAction} from './actions/import';
import {hardRefreshAction} from './actions/hardrefresh';
export const makeDataConnector = (client: DataConnectorClient) =>
new DataConnector(client).defineAsynchronousAction(DataAction.Import, importAction).defineAsynchronousAction(DataAction.HardRefresh, hardRefreshAction).defineAsynchronousAction(DataAction.Poll, hardRefreshAction);
Test hard refresh
With both the editor-extension and data-connector running, you should be able to test whether or not your card will refresh when opening the document. First, exit the document you have imported a task onto. Next, make some changes to the task you imported. Lastly, wait at least five minutes and open the document again. You should see the changes you made reflected in your imported card after a short period.
Test polling
With both the editor-extension and data-connector running, you should also be able to test whether or not your card is staying in sync through the polling action. While the document is open, make a change to your task in Todoist. Next, wait for around 30 seconds, and you should see your task update.
Pushing changes back to Todoist
When changes are made to Lucidcards linked to an external source, Lucid's services will automatically inform your data-connector of these changes. You can use this information to update the Task in the external data source.
Declaring the patch action
Declare the patch action by adding
{
//...
"dataConnectors": [
{
"name": "todoist",
"oauthProviderName": "todoist",
"callbackBaseUrl": "http://localhost:3001?kind=action&name=",
"dataActions": {
"Import": "Import",
"Patch": "Patch"
}
}
]
}
Then add
...
export enum DataAction {
Import = 'Import',
Patch = 'Patch'
}
Implementing the patch action
First, you will need to convert the data sent by Lucid, containing changes made to the cards, to a format that can be sent to Todoist to update the task. Add the following functions to
import {UpdateTaskArgs as TodoistUpdateTaskArgs} from "@doist/todoist-api-typescript";
...
export function lucidPatchToTodoistPatch(
data: Partial<TaskItemType>
): TodoistUpdateTaskArgs {
const dueDate = data[DefaultFieldNames.Due] as
| SerializedLucidDateObject
| null
| undefined;
return {
content: data[DefaultFieldNames.Content],
description: data[DefaultFieldNames.Description],
dueDatetime: dueDate ? lucidDateToTodoistDate(dueDate) : undefined,
};
}
function lucidDateToTodoistDate(date: SerializedLucidDateObject): string {
if ('ms' in date) {
return new Date(date.ms).toISOString();
}
return new Date(date.isoDate).toISOString();
}
Now, in the actions folder, create a new file named
import {TodoistApi as TodoistClient} from '@doist/todoist-api-typescript';
import {DataConnectorPatchAction, PatchItems} from 'lucid-extension-sdk/dataconnector/actions/action';
import {PatchChange} from 'lucid-extension-sdk/dataconnector/actions/patchresponsebody';
import {DefaultFieldNames} from '../../../common/names';
import {lucidPatchToTodoistPatch, TaskItemType} from '../collections/taskcollections';
export const patchAction: (action: DataConnectorPatchAction) => Promise<PatchChange[]> = async (action) => {
const todoistClient = new TodoistClient(action.context.userCredential);
return await Promise.all(
action.patches.map(async (patch) => {
const change = patch.getChange();
await Promise.all([updateTodoistTasks(patch.itemsChanged, change, todoistClient)]);
return change;
}),
);
};
async function updateTodoistTasks(itemsChanged: PatchItems, change: PatchChange, todoistClient: TodoistClient) {
await Promise.all(Object.entries(itemsChanged).map(async ([primaryKey, updates]) => {
try {
const taskId = JSON.parse(primaryKey) as string;
if (taskIsCompleted(updates)) {
await todoistClient.closeTask(taskId);
} else if (taskShouldBeReOpened(updates)) {
await todoistClient.reopenTask(taskId);
}
const todoistParams = await lucidPatchToTodoistPatch(updates);
if (Object.keys(todoistParams).length > 0) {
await todoistClient.updateTask(taskId, todoistParams);
}
} catch (err) {
change.setTooltipError(primaryKey, 'Failed to update Todoist');
console.error('error patching', err);
}
}));
}
function taskIsCompleted(data: Partial<TaskItemType>): boolean | undefined {
return data[DefaultFieldNames.Completed];
}
function taskShouldBeReOpened(data: Partial<TaskItemType>): boolean {
return data[DefaultFieldNames.Completed] === false;
}
Now, you can register the patch action with the data-connector. Update
import { DataConnector, DataConnectorClient } from "lucid-extension-sdk";
import { importAction } from "./actions/import";
import { DataAction } from "../../common/names";
import { patchAction } from "./actions/patch";
export const makeDataConnector = (client: DataConnectorClient) =>
new DataConnector(client)
.defineAsynchronousAction(DataAction.Import, importAction)
.defineAction(DataAction.Patch, patchAction);
Perform an update
With both the editor-extension and data-connector running, you should be able to make edits to the Lucid cards and have the corresponding task in Todoist be updated.
Creating new tasks in Todoist
The patch action created above can also be used to create new tasks in the external data source. The patch Lucid sends to your data-connector will contain information about any new data-linked cards the user created. You can use this data to create a new task in Todoist.
Updating the patch action
The data-connector will need to convert the creation data it receives from Lucid into data that can be sent to Todoist to create the task. In
import {AddTaskArgs as TodoistAddTaskArgs} from "@doist/todoist-api-typescript";
...
export function lucidPatchToTodoistCreationData(data: Partial<TaskItemType>): TodoistAddTaskArgs {
const {content, description, dueDatetime} = lucidPatchToTodoistPatch(data);
return {
content: content ?? '', // Todoist requires this to be defined
dueDatetime: dueDatetime ?? undefined, // The rest of these need to be undefined instead of null for creation
description: description,
};
}
Now, you need to add a function which creates Todoist tasks to
...
async function createTodoistTasks(
itemsAdded: PatchItems,
syncCollectionId: string,
change: PatchChange,
todoistClient: TodoistClient,
) {
await Promise.all(Object.entries(itemsAdded).map(async ([oldPrimaryKey, additions]) => {
try {
const formattedCreate = {
...lucidPatchToTodoistCreationData(additions),
};
const taskResponse = await todoistClient.addTask(formattedCreate);
change.collections.push({
collectionId: syncCollectionId,
items: {[oldPrimaryKey]: getFormattedTask(taskResponse)},
itemsDeleted: [],
});
} catch (err) {
change.setTooltipError(oldPrimaryKey, 'Failed to create task in Todoist');
console.error('Error creating item', err);
}
}));
}
Finally, update the
...
export const patchAction: (action: DataConnectorPatchAction) => Promise<PatchChange[]> = async (action) => {
const todoistClient = new TodoistClient(action.context.userCredential);
return await Promise.all(
action.patches.map(async (patch) => {
const change = patch.getChange();
await Promise.all([
updateTodoistTasks(patch.itemsChanged, change, todoistClient),
createTodoistTasks(patch.itemsAdded, patch.syncCollectionId, change, todoistClient),
]);
return change;
}),
);
};
...
Creating the card creation callout
With the data-connector set up, you can now implement an entry point for the user to input information to create a new card. In
export class TodoistTaskCreator {
private todoistClient: TodoistClient;
constructor(private readonly editorClient: EditorClient) {
this.todoistClient = new TodoistClient(editorClient);
}
}
First, you will need to define all the fields that should be shown to the user when they attempt to create a new Todoist card. The
export class TodoistTaskCreator {
...
private readonly contentField = "content";
private readonly projectField = "project";
public async getInputFields(inputSoFar: Map<string, SerializedFieldType>): Promise<ExtensionCardFieldDefinition[]> {
const projects = await this.todoistClient.getProjects();
const fields: ExtensionCardFieldDefinition[] = [
{
name: this.contentField,
label: 'Content',
type: ScalarFieldTypeEnum.STRING,
constraints: [{type: FieldConstraintType.REQUIRED}],
},
{
name: this.projectField,
label: 'Project',
type: ScalarFieldTypeEnum.STRING,
default: projects[0]?.id,
constraints: [{type: FieldConstraintType.REQUIRED}],
options: projects.map((project) => ({label: project.name, value: project.id})),
},
];
return fields;
}
}
Once the user provides the information you will need to pass this information off to the data-connector so that a new task can be created in Todoist. Since updates in Lucid are automatically sent to the data-connector, all you will need to do is update the task collection with the new data. This is done in the
export class TodoistTaskCreator {
...
public async createCardData(
input: Map<string, SerializedFieldType>,
): Promise<{collection: CollectionProxy; primaryKey: string}> {
const projectId = input.get(this.projectField);
if (!isString(projectId) || !projectId) {
throw new Error('No project selected');
}
let collection: CollectionProxy;
try {
// Check if the Tasks collection has already been created in the // data source, with a 1ms timeout. If the collection doesn't exist yet, // an exception will be thrown.
collection = await this.editorClient.awaitDataImport(DataConnectorName, projectId, CollectionName, [], 1);
} catch {
// No collection exists yet. Ask the data connector to perform an import of // an empty list of tasks, just to get the collection created along with whatever other // metadata you might need to set up there.
await this.editorClient.performDataAction({
dataConnectorName: DataConnectorName,
syncDataSourceIdNonce: projectId,
actionName: DataAction.Import,
actionData: [],
asynchronous: true,
});
// And now wait for that empty import to complete, getting a reference to the // resulting collection.
collection = await this.editorClient.awaitDataImport(DataConnectorName, projectId, CollectionName, []);
}
// Add the user input data to the collection. These changes will be sent to
// the data-connector
const primaryKeys = collection.patchItems({
added: [
{
[DefaultFieldNames.Project] : projectId,
[DefaultFieldNames.Content]: input.get(this.contentField),
},
],
});
if (primaryKeys.length != 1) {
throw new Error('Failed to add new card data');
}
return {collection, primaryKey: primaryKeys[0]};
}
}
Finally, construct an object of this class in
export class TodoistCardIntegration extends LucidCardIntegration {
// ...
public addCard = new TodoistTaskCreator(this.editorClient);
}
Create a new task
With both the editor-extension and data-connector running, you should be able to create new Lucid cards along with the corresponding task in Todoist.
Examples
The source code for a basic card integration not linked to any third party can be found here. This can serve as a template for you to build a card integration.
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.