Your first SCAYLE Add-On
Create your first Add-On
- Add-On
- Tech
Mikheil Kupatadze
Senior Frontend Developer
Any new Add-on application will involve setup and configuration similar to the demo add on. Each Add-on app requires a manifest file where application data and routes are set.
Specific app functionality can be defined using the Admin API (for access to the backend services, such as shop/product data) and the Add-on API (for access to the SCAYLE Panel session data, such as user information).
Add-on Apps are:
Applications should be wrapped as a single-spa application. In other words, it is an object containing three methods:
bootstrap
mount
unmount
bootstrap
is called the first time the application is mounted while mount
and unmount
contain the logic for mounting and un-mounting the application.
On the initial page load, the SCAYLE Panel will request the manifest file of each active add-on. Inside the manifest, the Add-on app will be registered with single-spa.
Single-spa then automatically loads or unloads applications on URL changes. When the URL matches /add-ons/{addOnName}/*
or /shops/:id/{addOnName}/*
the Add-on with that name is loaded.
Each Add-on should have a unique URL. In the case that the app URL of multiple add ons is the same, opening one of the add ons results in loading all of them. This can cause undesirable behaviour such as the sidebar loading multiple times.
Each Add-on app must have a manifest file. This is the file that the SCAYLE Panel fetches in order to load the application. The manifest should export a single function manifestRegistration
.
export default manifestRegistration;
The manifest should be formatted in the System.register
module format. With webpack, this can be configured by setting output.library.type
to be system
(or set rollup's format to "system").
When using Webpack
System.js
format, it will not work properly.This library provides an easy way to set the public path on the fly. Simply install the package and add this line to the top of your manifest file.
import "systemjs-webpack-interop/auto-public-path";
Here’s an example of an app manifest (from the demo add-on):
// This config function does nothing but return its input
// But it does provide some helpful type hints
import { config, RouteDefinition } from '@scayle/add-on-utils';
import { AddOnRoute, routes } from './router';
import { GroupRouteDefinition } from './types';
import { ADD_ON_ID, generateGroupName } from './utils';
const applyDefaultRouteProps = (routes: RouteDefinition[]) => routes.map(route => ({...route, sidebar: route.sidebar === null ? null : ADD_ON_ID}));
const mappedRoutes = routes.map(originalRoute => {
const route = JSON.parse(JSON.stringify(originalRoute)) as AddOnRoute;
const children = route.children;
const meta = route.meta as RouteDefinition;
if(children && children.length && !meta.children) {
meta.children = children.map(childRoute => {
const pathArray = childRoute.path.split("/");
const pathForManifest = pathArray[pathArray.length - 1];
// if the path for the manifest also contains the path of the parent,
// the active item highlighting in the sidebar does not work that's what the lines above are for
return "/" + pathForManifest;
});
}
return meta;
});
const generalRoutes: GroupRouteDefinition[] = [
{
id: 'general-group',
name: {
'en': 'General',
'de': 'Allgemeines'
},
group: generateGroupName('general'),
isGroup: true,
},
...mappedRoutes
];
const manifestRegistration = config(function (registerApplication, registerRoutes) {
registerApplication({
name: ADD_ON_ID,
// Make sure to use a dynamic import to create a code-split point
// and minimize the size of the manifest since it is loaded on every page
app: () => import('./add-on'),
});
registerRoutes({
[ADD_ON_ID]: [
...applyDefaultRouteProps(generalRoutes as RouteDefinition[]),
]
})
});
// !!!!!DO NOT CHANGE LINE BELOW
// IT IS USED FOR HOT RELOADING ON CLOUD PANEL ADDON
// AND WILL BREAK IF THE LINE BELOW IS CHANGED
export default manifestRegistration;
For the routes, the routes.ts
file is the single source of truth. Import and map routes in the manifest.ts
file as shown in the example below. Properties required for the manifest routes should be defined in the route meta of routes.ts
. To disable the SCAYLE Panel sidebar, set route.meta.sidebar
to null.
To define the path of route children, use one of the following formats: :id
OR /table-listing/:id
.
Here’s an example router.ts
file:
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { GroupRouteDefinition } from './types';
import { ADD_ON_ID, BASE_URL, generateGroupName } from './utils';
// Use dynamic imports to enable code-splitting
const DashboardPage = () => import('./pages/DashboardPage.vue');
const AlertsPage = () => import('./pages/AlertsPage.vue');
const FormPage = () => import('./pages/FormPage.vue');
const TableListing = () => import('./pages/TableListing.vue');
const ComponentsPage = () => import('./pages/ComponentsPage.vue');
export type AddOnRoute = Omit<RouteRecordRaw, "meta"> & { meta: GroupRouteDefinition };
export const routes: AddOnRoute[] = [
{
name: 'dashboard',
path: '/',
component: DashboardPage,
meta: {
id: 'dashboard',
name: {
'en': 'Dashboard',
'de': 'Armaturenbrett'
},
icon: 'dashboard',
path: BASE_URL + '/',
sidebar: ADD_ON_ID,
group: generateGroupName('general'),
}
},
{
path: '/table-listing',
component: TableListing,
children: [
{
name: "table-detail",
path: ":id",
component: TableListing
}
],
meta: {
id: 'table-listing',
name: {
'en': 'Table Listing',
'de': 'Tabellenauflistung'
},
icon: 'data-table',
path: BASE_URL + '/table-listing',
sidebar: ADD_ON_ID,
group: generateGroupName('general'),
}
},
{
path: '/form',
component: FormPage,
meta: {
id: 'form',
name: {
'en': 'Form',
'de': 'Form'
},
icon: 'search',
path: BASE_URL + '/form',
group: generateGroupName('general'),
}
},
{
path: '/alerts',
component: AlertsPage,
meta: {
id: 'alerts',
name: {
'en': 'Alerts',
'de': 'Alerts'
},
icon: 'warning',
path: BASE_URL + '/alerts',
group: generateGroupName('general'),
}
},
{
path: '/components',
component: ComponentsPage,
meta: {
id: 'components',
name: {
'en': 'Components',
'de': 'Components'
},
icon: 'ufo',
path: BASE_URL + '/components',
group: generateGroupName('general'),
}
},
];
export default createRouter({
routes: routes as RouteRecordRaw[],
// Make sure to set the base because all Add-on pages will be under add-ons/demo
history: createWebHistory(BASE_URL),
});
##Add-on User Flow
How logged-in user info flows between the SCAYLE Panel and add-ons.
Each Add-on defines its own permissions. They are published to the SCAYLE Panel via the Add-on API. Permissions can then be assigned to roles, which are attached to users, using the SCAYLE Panel.
A permission name follows a simple structure to be displayed correctly inside the role creation.
{{add-on-name:customer-app}}__{{headline:administration}}__{{permission:manage}}
For example, you may want to have your own section within the permissions that is called “Administration”, and within this is the permission “Manage administration”. The full permission name could look as follows: customer-app__administration__manage
.
Read more about the translation of permissions inside the section Translations.
Once the user is logged into the SCAYLE Panel, all registered add-ons will have access to the current user’s Authorization Token (available under window.Cloud.Config.auth.token
). That Authorization Token can then be used by your add-on, to authorize the user against the SCAYLE Panel API in your backend application.
The Authorization header must start with the word Bearer (e.g., Authorization: Bearer 3|yUt2B8yE*****Cg4RRwWJwVTIn6RRyQ9
).
This information allows the Add-on developer to initiate their own dedicated Session Token for calls to their own API. We recommend a dedicated route for this initial authentication process (e.g., /api/v1/external/add-ons/user
).
The response only returns the information that belongs to the add-on. For example, User Custom Data only returns the permissions and custom data that was added through the add-on. All other information is based on the SCAYLE Panel user.
{
"id":1,
"name":"John Doe",
"email":"[email protected]",
"locale":"de_DE",
"timezone":"Europe/Berlin",
"phoneNumber":"12345678",
"companies":[
1,
2
],
"allowedShopCountries":[
100,
200
],
"allowedShops":[
1,
2
],
"allowedMerchants":[
1,
2
],
"permissions":[
"create",
"delete"
],
"customData":{
"alias":"Max",
"color":"red"
},
"roles":[
"developer",
"administrator"
],
"createdAt":"2023-01-01T00:00:00.000000Z",
"updatedAt":"2023-01-02T05:00:00.000000Z"
}
The Add-on API translations endpoint allows developers to easily create and modify translations for add-ons. Translations can be applied to the Add-on permissions, title, and descriptions. You can not, for example, apply a buttons translation.
Translations are divided into two types:
add-on-dev-guide__dashboard__view
). Those translations can be seen in the Settings/Roles section where the permissions are configuredtitle
or description
{
"translations":{
"title":{
"de_DE":"Developer Guide Test",
"en_GB":"Developer Guide Test"
},
"description":{
"de_DE":"Developer Guide Test",
"en_GB":"Developer Guide Test"
}
}
}
For System translations, we first need to set up some permissions.
{
"permissions":[
"add-on-dev-guide__dashboard__view",
"add-on-dev-guide__dashboard__edit"
]
}
{
"translations":{
"permission.add-on-dev-guide":{
"de_DE":"Developer Guide Test",
"en_GB":"Developer Guide Test"
},
"permission.add-on-dev-guide__dashboard":{
"de_DE":"Dashboard",
"en_GB":"Dashboard"
},
"permission.add-on-dev-guide__dashboard__view":{
"de_DE":"Dashboard Ansehen",
"en_GB":"View Dashboard"
},
"permission.add-on-dev-guide__dashboard__edit":{
"de_DE":"Dashboard Editieren",
"en_GB":"Edit Dashboard"
}
}
}
Follow our Add-on API Guide to see how you can create translations, from your backend application.
These translations are then visible under Settings > Users management > Roles
for the corresponding add-on.
It is possible for add-ons to receive messages when certain events happen in the SCAYLE Panel (i.e, user updates, role updates).
In order to receive these messages, two conditions need to be met:
That callback URL should accept a POST request with content type: application/json
.
To increase security of incoming webhooks calls, the best option is to whitelist the domains that the calls can come from (e.g., *.scayle.cloud
).
Message Structure
Every message sent to the callback URL will have the same structure:
Property | Type | Notes |
---|---|---|
event | string | Contains the event key, which will be used by the Add-on to determine which flow should be used in any case. Dot-separated string with the following structure: {model}.{action} (e.g., user.created ) |
context | object | Contains any exposed model attribute. Its content varies from model to model, but all events related to the same entity will have the same fields. |
id | int | ID of the entity that has been affected (i.e., the user ID for User) |
Model Definition
Every model that will post messages is defined below.
Deleted events will not contain any field in their context attribute of the message.
Model | User |
---|---|
Possible events | UserCreated , UserUpdated , UserDeleted , UserLoggedOut |
Context fields | none |
Structure example | {"id": 4, "event": "UserCreated", "context": {},} |
Model | Role |
---|---|
Possible events | RoleCreated , RoleUpdated , RoleDeleted |
Context fields | none |
Structure example | { "id": 6, "event": "RoleUpdated", "context": {"userIds": [3,5,6]},} |
Custom data can be used to store additional data that you need directly after initialization in the SCAYLE Panel handshake. It will appear in the user property of the context object that is passed to the add-on.
With custom data, you could configure any information on the user. For example, you could add a specific property to all users that have the role administrator.
The Notification API allows Add-on Developers to interact with the logged-in users of the panel. Currently, only the generic message is available.
Follow our Add-on API Guide to see how you can create user notifications, from your backend application.
Your first SCAYLE Add-On
Mikheil Kupatadze
Senior Frontend Developer