Flotiq UI Plugins development¶
If you find Flotiq UI not enough, you can write your own extensions, that will suit your specific needs.
Flotiq UI plugins can be used to enhance our UI experience within your account, including (but not limited to):
- Changing styles of the form
- Replacing existing inputs in forms with dedicated widgets (e.g. star rating radio instead of radio buttons, a tool to visually place items on 2d canvas with coordinates saved to a list)
- Modifying preview of certain properties in table-view
- Integrating with tools like ChatGPT to generate text descriptions
- Adding status panels that integrate with your analytics tool
- Expanding the functionality of existing inputs by adding a visual preview of the results
- Introducing custom validation rules before objects can be saved
Basics¶
The Flotiq extensions system is based on the idea of events. The plugin first registers itself, and after that, it registers event handlers for the actions it wants to intercept.
Once event handlers are registered, the plugin is ready to help us. For example by rendering it as a React component via <ElementFromPlugin>
:
Writing the code¶
Here's a general structure of the plugin code:
FlotiqPlugins.add({ // 1. Registration
id: '<unique plugin id>',
name: '<human readable name for the plugin>',
version: '<semver version>',
permissions: [/* list of permissions, see below */],
}, function (handler, apiClient, globals) { // 2. Initialization
handler.on('flotiq::event1', (eventParams) => { // 3. Event handling
/* event handling code */
})
})
What happens here?
-
Registration - First, the plugin needs to introduce itself to Flotiq. Most of the information here is for end-users, so that they can review what kind of plugins they are running. There are several pieces of info that are more important to the plugin API:
id
- this must be a unique string for all plugins. Only one plugin with a givenid
can be registered. If subsequent registration is executed with the sameid
, the previous plugin instance will be removed. We recommend prefixing each pluginid
with your company name to avoidid
conflicts.name
* - A user friendly name of the plugin. It will be shown in plugin management pageurl
* - Full URL to the js file that contains the plugin.version
* - Current version of the plugin. It is required for Flotiq to determine if the version used by the user is still up to date.permissions
- if the plugin requires access to user data via Flotiq API, it must present a set of required permissions. This is the only way, to get anapiClient
object that will allow you to access user content or schema.
* These fields are not required during plugin development, but must be present in plugin manifest when plugin is published and loaded via manifest entry
-
Initialization - After registration, your plugin will be initialized with a provided callback. This callback receives two arguments:
handler
- an event handler for your plugin. It will allow you to attach to multiple UI events.apiClient
- an API client with a set of properties that allow you to access user data.
This is the best place to perform initialization work. E.g. inject
<style>
elements with your CSS classes. -
Event handling - After all initialization code is executed, your plugin will wait for events from Flotiq UI. Each time the event occurs in Flotiq, your event callback will be executed. Each event has a different set of parameters and expects different results. For example
::render
events will expect HTML content as a result, while::config
events will not take any result and expect you to modify existing config. For details, please refer to Flotiq Event Types.
API permissions¶
Often plugins require access to user data. E.g. to display options stored in another content type, or to display images from a relation field.
Flotiq UI plugins are allowed to access API only with pre-prepared API client, after declaring the scope of access.
Important: plugins are not using own API Keys, nor there are direct fetch
calls from plugin code to API. Instead, plugins must use the provided API client. All of the write actions will be registered as if a user made the change.
To be able to do it, a plugin needs to request API access with permissions
field during registration:
FlotiqPlugins.add({
id: '<unique plugin id>',
name: '<human readable name for the plugin>',
version: '<semver version>',
permissions: [
{type: 'CO', canRead: true, ctdName: '*'},
{type: 'CO', canWrite: true, canCreate: true, canDelete: true, canRead: true, ctdName: 'blogpost'},
{type: 'CTD', canRead: true, ctdName: '_media'},
{type: 'CTD', canRead: true, ctdName: '_tag'},
]
}, /*...*/)
In the example above, we are requesting the following access:
- an ability to read all content objects
- an ability to read, modify, delete and create objects of type
blogpost
- an ability to read the schema of
_media
(we can read_media
objects already due to the first access rule. ) - an ability to read the schema of
_tags
Now, we can utilize this info in our event handlers:
FlotiqPlugins.add(
{
/* ... */
permissions: [
/*...*/
],
},
function (handler, apiClient) {
handler.on('flotiq::event1', () => {
// Load Content Type Definition for _media objects
apiClient._media.getContentType().then(({ body, ok }) => {
if (ok)
console.log('media get schema:', body);
else
console.error('media get schema error:', body);
});
// Load _media objects
apiClient._media.get('<media id>').then(({ body, ok }) => {
if (ok)
console.log('media get response:', body);
else
console.error('media get error:', body);
});
// Save new blogpost object
apiClient.blogpost.post({title: 'title', content: 'content'}).then(({ body, ok }) => {
if (ok)
console.log('blogpost create response:', body);
else
console.error('blogpost create error:', body);
});
});
},
);
Examples¶
Available classes and events¶
Caching¶
As a plugin author, you are responsible for caching relevant results on your end. There are two areas that will require caching in most scenarios.
::render
and ::add
result caching¶
Flotiq UI updates whenever your plugin returns something new. Consider the following code:
//...
handler.on('flotiq.grid.cell::render', ({ contentType, accessor, data }) => {
//...
// Create HTML image element
const div = document.createElement('div');
div.textContent = data.toString().toUpperCase();
return div;
});
//...
In this example, each time the grid cell is rendered we are creating a new HTML element and populating it with data. While this operation does not take long, we are doing it for all grid cells, which may sum up to lower performance and many unnecessary UI re-renders.
To avoid this, it's a good practice to set up an element cache:
const elementCache = {};
//...
handler.on(
'flotiq.grid.cell::render',
({ contentType, accessor, data, contentObject }) => {
//...
const elementCacheKey = `${contentType.name}-${contentObject.id}-${accessor}`;
// Check if the element for this cell exists already.
let div = elementCache[elementCacheKey];
if (!div) {
// If not, create HTML image element and ut it into cache
div = elementCache[elementCacheKey] = document.createElement('div');
}
// Update content
div.textContent = data.toString().toUpperCase();
return div;
},
);
//...
Here we added an in-memory cache for created div elements. Each element in the cache is indexed by content type, id of the object and the name of the property. If the div element for the particular cell already exists, it will be returned instead of creating a new one. This will not only save us some memory and CPU but also avoid unnecessary re-renders of the UI!
This can be easily so that elements are removed from the cache, once they are removed from the DOM. We can also cache more data alongside the element (for example React app root object)
API call results¶
Often you'll be calling Flotiq API to query some data. There are views in which the data will not change often though. One such view is a content grid table where we're just displaying some data and no updates are done.
Let's consider the example below:
const elementCache = {};
handler.on('flotiq.grid.cell::render', ({ contentType, accessor, data, contentObject }) => {
// Don't do anything if we're not rendering blogpost -> media field
if (contentType?.name !== 'blogpost') return null;
if (accessor != 'headerImage') return null;
const elementCacheKey = `${contentType.name}-${contentObject.id}-${accessor}`;
const imageRelation = data[0];
let img = elementCache[elementCacheKey];
if (!img) img = elementCache[elementCacheKey] = document.createElement('img');
// Load media object using api client
// Important - *::render and *::add event handlers
// may not be async/await functions, so we need to use
// .then callback to add src attribute
const imageId = imageRelation.dataUrl.match(/[^/]+$/)[0];
client._media.get(imageId).then(({ body }) => {
// Once media info is loaded, set image url
const imgUrl = client.getMediaUrl(body, 30, 30);
img.setAttribute('src', imgUrl);
});
return img;
});
Here, our code creates an img
element. Then we extract mediaId
from dataUrl
and download info about the media file. Finally, we populate the src
attribute with an image URL based on the _media
content object.
This will display correct data, however, we'll download _media
object for each row of the grid, even in some cases the same image may occur more than once. To improve user experience, we should keep some of this info in a local cache and reuse it when we can.
Now let's take a look at an improved example:
const elementCache = {};
const mediaPromiseCache = {};
handler.on('flotiq.grid.cell::render', ({ contentType, accessor, data, contentObject }) => {
// Don't do anything if we're not rendering blogpost -> media field
if (contentType?.name !== 'blogpost') return null;
if (accessor != 'headerImage') return null;
const elementCacheKey = `${contentType.name}-${contentObject.id}-${accessor}`;
const imageRelation = data[0];
let img = elementCache[elementCacheKey];
if (!img) img = elementCache[elementCacheKey] = document.createElement('img');
// Load media object using api client
// Important - *::render and *::add event handlers
// may not be async/await functions, so we need to use
// .then callback to add src attribute
const imageId = imageRelation.dataUrl.match(/[^/]+$/)[0];
let mediaPromise = mediaPromiseCache[imageid];
if (!mediaPromise) {
mediaPromise = mediaPromiseCache[imageid] = client._media.get(imageId);
}
mediaPromise.then(({ body }) => {
// Once media info is loaded, set image url
const imgUrl = client.getMediaUrl(body, 30, 30);
img.setAttribute('src', imgUrl);
});
return img;
});
We added a similar cache object to the element cache. Each cache element is a promise that is returned from the API client. We create this promise only once for each media id
. Not only we will avoid pulling the same image data in parallel, but we will also re-use this data if such an image is used on other grid pages.
Acting when an element is attached to the DOM tree¶
If you need to make any changes to the element after it was attached to the DOM tree, you can use flotiq.attached
DOM event.
Important: This event is only fired on the root element returned by the plugin. Elements inside will not receive this event.
const element = document.createElement('span');
console.log(element.parentElement); // will return null
element.addEventListener('flotiq.attached', () => {
console.log(element.parentElement); // will return parent <div>
});
return element;
Acting when an element is detached from the DOM tree¶
If you want to do something after detaching an element from the DOM tree (e.g. clear the element cache), you can use flotiq.detached
DOM event.
Important: This event is only fired on the root element returned by the plugin. Elements inside will not receive this event.
const element = document.createElement('span');
element.addEventListener('flotiq.detached', () => {
console.log('element removed'); // will be fired after element is detached
});
return element;
Multiple attaches/detaches
Your HTML element may be attached and detached multiple times, depending on the view and place being rendered. Be prepared to handle those situations accordingly. E.g. if you keep your elements in a cache across multiple renders, consider waiting at least 50ms before the element is removed from the cache.
Plugin Installation¶
Once you have your plugin written, you have several options to install it into Flotiq UI. Multiple instances of the plugin with the same id
will override each other, so you can easily update your plugin as needed.
Temporary installation using js¶
Important This way, you'll register a plugin until you log out or refresh a browser tab.
- Go to the Flotiq page and log in to your account
- Open the developer console for the browser
- Paste your plugin code into the console
- (optional) work on the code
- (optional) paste the code again
Temporary installation using URL to js file¶
Important This way, you'll register a plugin until you log out or refresh a browser tab.
Prerequisites: Your plugin script needs to be accessible via http(s) URL from your browser.
- Go to the Flotiq page and log in to your account
- Open the developer console for the browser
- Execute the following command:
{ data-search-exclude } For example:
FlotiqPlugins.loadPlugin('<unique script id>', '<url to file>');
FlotiqPlugins.loadPlugin( 'mycompany.my-local-plugin', 'http://localhost:3000/index.js', );
Permanent installation for the organization¶
Prerequisites: Your plugin script needs to be accessible via the http(s) URL from the browser of any user in your organization. In this case, localhost
serving may not be enough unless you know for sure you are the only user of the plugin!
Your plugin also must contain all required information in the plugin-manifest.json
file. Manifest file has the same structure as the object provided when plugin registers its event handlers, however, some fields are no longer optional when manifest is provided.
See an example of `plugin-maniest.json` that can be used to register new plugin
{
"id": "my-organization.example-plugin",
"name": "An Example plugin",
"description": "This is plugin created by My Organization to handle an example workflow for all data types",
"version": "1.0.0",
"repository": "https://github.com/my-organization/flotiq-example-plugin",
"url": "https://example.com/index.js",
"permissions": [
{
"ctdName": "*",
"canRead": true,
"type": "CO"
}
]
}
For more details on the required fields, see PluginInfo.
If your plugin is ready for wider use within your organization:
- Go to the Flotiq page and log in to your account
- On the left-hand side select Plugins
- Paste the URL to your
plugin-manifest.json
file in the Custom plugin field - Press the Add button (blue "+" icon) next to the field. Your plugin should now be visible in the list of available plugins
- Add your plugin to the organization (gray "+" to the right of the plugin name) and enable it.