Extending Clients with Plug-ins
Extend a client based on the Developer Libraries with additional functions via plug-ins.
Table of Contents
Introduction
The PluginsService provided by the framework library allows for extending clients with custom HTML code via a configuration file (s. API documentation for detailed information). The corresponding configuration file is managed via the Web-API Gateway endpoints. It is a simple and fast procedure without the necessity of rebuilding and deployment.
Endpoints for Plug-in Configuration Management
The Web-API gateway provides separate endpoints for the management of tenant-specific and global configuration files. This applies to the plug-in configuration files as well. Use plugin-config
as value for the path parameter name
. Import an empty JSON file to remove the configuration.
Global:
- GET /api-web/api/system/resources/config/{name} - Retrieves the global configuration file specified by
name
. - POST /api-web/api/system/resources/config/{name} - Updates the global configuration file specified by
name
with the data passed in JSON format in the request body.
Tenant-specific:
- GET /api-web/api/admin/resources/config/{name} - Retrieves the tenant-specific configuration file specified by
name
. - POST /api-web/api/admin/resources/config/{name} - Updates a tenant-specific configuration file specified by
name
with the data passed in JSON format in the request body.
Structure of the Configuration File
The configuration file is in JSON format. The disabled
key controls whether the client will use the defined plug-ins or not. The load function is executed once the client is loaded. The following example shows how to disable plug-ins for users with specific roles and how to change the application theme via custom styles. The other sections are described below.
{ "disabled": "() => !api.session.user.hasRole('YUUVIS_TENANT_ADMIN')", "load": "() => api.util.styles('body, body.dark { --color-accent-rgb: 131, 108, 172; --color-accent: rgb(var(--color-accent-rgb)); --theme-logo-width: 50px; --theme-background: url(https://images.unsplash.com/photo-1460411794035-42aac080490a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80); --theme-logo: url(https://images.unsplash.com/photo-1460411794035-42aac080490a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=50&q=80); --theme-logo-big: url(https://images.unsplash.com/photo-1460411794035-42aac080490a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=250&q=80); }', '_pluginStyles')", "links": [], "states": [], "actions": [], "viewers": [], "extensions": [], "triggers": [], "translations": { "en": {}, "de": {} } }
Detailed descriptions of the properties can be found in the documentation (s. PluginConfigList interface in framework library).
Plug-in Links
Configured links
with a path
and label
can be hooked into the main menu or the settings menu. The label must be configured in the translations
section as shown below.
{ "links": [ { "id": "yuv.custom.link.home_yuuvis", "label": "yuv.custom.action.home_yuuvis.label", "path": "https://help.optimal-systems.com/yuuvis/home_yuuvis_en.html", "matchHook": "yuv-sidebar-navigation|yuv-sidebar-settings" } ] }
Detailed descriptions of the properties can be found in the documentation (s. PluginLinkConfig interface in framework library).
Plug-in States
States are new views with their own URL that are mostly reached by clicking a main menu item (see Extend Links). The following example shows how to integrate the user management view of yuuvis® architect as a new view into the client. This view can be opened via a new main menu item 'User management.' This item is offered if the user has the YUUVIS_TENANT_ADMIN role.
{ "states": [ { "id": "yuv.custom.state.architect.users", "label": "yuv.custom.state.architect.users", "path": "architect/users", "matchHook": "yuv-sidebar-navigation", "canActivate": "(currentRoute, currentState) => {return this.session.getUser().authorities.includes('YUUVIS_TENANT_ADMIN');}", "canDeactivate": "(component, currentRoute, currentState, nextState) => true", "plugin": { "src": "https://kolibri.enaioci.net/architect/users", "html": "<style>iframe[src=\"https://kolibri.enaioci.net/architect/users\"] {display: block; height: calc(100% + 50px); margin-top: -50px;}</style>" } } ], "translations": { "en": { "yuv.custom.state.architect.users": "User management" }, "de": { "yuv.custom.state.architect.users": "Benutzermanagement" } } }
Detailed descriptions of the properties can be found in the documentation (s. PluginStateConfig interface in framework library).
Plug-in Actions
In general, object actions that are listed in the actions menu provide specific functions that manipulate the focused object, e.g., Download, Manage follow-ups, Open versions, ... in yuuvis® client as reference implementation. If you want to include all original actions, use "*"
instead of listing them individually. Note that the position of the listed actions (or the "*"
if all actions are specified) within the script is irrelevant. The following example shows the implementation of an action that lets you navigate to the dashboard.
{ "actions": [ "yuv-download-action", "yuv-delete-action", "yuv-upload-action", "yuv-move-action", { "id": "yuv.custom.action.home_yuuvis.list", "label": "yuv.custom.action.home_yuuvis.list", "description": "yuv.custom.action.home_yuuvis.description", "icon": "", "isExecutable": "(item) => item.id", "header": "yuv.custom.action.home_yuuvis.label", "subActionComponents": [ { "id": "yuv.custom.action.home_yuuvis.sub.simple", "label": "yuv.custom.action.home_yuuvis.simple", "description": "yuv.custom.action.home_yuuvis.description", "priority": 0, "icon": "", "group": "common", "range": "MULTI_SELECT", "isExecutable": "(item) => item.id", "run": "(selection) => this.router.navigate(['dashboard'])" }] } ] }
Detailed descriptions of the properties can be found in the documentation (s. PluginActionConfig interface framework library).
Plug-in Viewers
The Preview of the document file can be customized via a custom viewer configuration. The following example shows how to:
- redirect of office files to external service (Office 365 viewer)
- redirect of javascript and json files to monaco editor (line 18)
- redirect of text files to monaco editor (line 19)
- redirect of audio and video files to a custom video viewer (line 20)
- redirect of xml files to monaco editor (line 21)
- redirect of images to a custom image viewer (line 22)
- redirect of text and audio and video files to a native html viewer (line 23)
- customize the viewer path for each file (line 24)
{ "viewers": [ { "mimeType": [ "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.presentationml.template", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" ], "viewer": "externals/office?path=${path}#locale=${locale}&direction=${direction}&theme=${theme}&accentColor=${accentColor}&fileName=${fileName}&fileExtension=${fileExtension}" }, {"fileExtension": ["js", "json"], "viewer": "assets/_default/monaco/index.html?file=${file}&lang=${lang}"}, {"mimeType": ["text/plain"], "viewer": "assets/_default/monaco/index.html?file=${file}&lang=${lang}¶ms={\"lineNumbers\":\"off\"}"}, {"mimeType": ["audio/mp3", "video/mp4"], "viewer": "assets/_default/video/index.html?file=${path}&lang=${lang}"}, {"mimeType": ["text/xml"], "viewer": "assets/_default/monaco/index.html?language=xml&responseType=xml&file=${file}&lang=${lang}"}, {"mimeType": ["image/jpeg", "image/png", "image/x-ms-bmp"], "viewer": "assets/_default/image/index.html?file=${file}&lang=${lang}"}, {"mimeType": ["text/plain", "audio/mp3", "video/mp4"], "viewer": "() => parameters.path"}, {"mimeType": "*", "type": "extend", "viewer": "() => parameters.mimeType === "text/plain" ? parameters.viewer + '&extend'" : parameters.defaultViewer}, ] }
Detailed descriptions of the properties can be found in the documentation (s. PluginViewerConfig interface in framework library).
Plug-in Extensions
The standard aspects of the object details offer general views of the data of the object: an Overview of all metadata with the possibility to edit the custom Metadata, a Preview of the document file and a History of system events. If you need more views of data that are residing in other services, you can add more such aspects. Custom extensions are also available for the Content Preview or Search Filter Panel. The following example shows:
- Tab component – how to load a custom html page via iframe (line 4)
- Tab component – custom html code and styles rendition (line 10)
- Tab component – a custom component rendition (line 20)
- Content preview – custom buttons to select the next/previous grid row (line 30)
- Search filter panel – a custom filter with the dynamic values
CURRENT_USER
andTODAY
(line 39) - Search filter panel – a custom filter group generated from a static catalog (line 68)
- Search filter panel – a custom filter group generated from a dynamic catalog (line 79)
- Search filter panel – custom filter groups generated from all available catalogs (line 90)
- Search filter panel – a custom filter with a dynamic date range - next 28 days (
TODAY,
(line 103)TODAY+27)
{ "extensions": [ { "id": "yuv.custom.extension.home_yuuvis", "label": "yuv.custom.extension.home_yuuvis.label", "matchHook": "yuv-result|yuv-object", "plugin": { "src" : "https://help.optimal-systems.com/yuuvis/home_yuuvis_en.html" } }, { "id": "yuv.custom.extension.home_yuuvis.complex", "label": "yuv.custom.extension.home_yuuvis.description", "matchHook": "yuv-result|yuv-object", "plugin": { "html": "<a href='https://help.optimal-systems.com/yuuvis/home_yuuvis_en.html'> yuuvis Home </a> <button onclick=\"api.router.navigate(['dashboard'])\">Go to dashboard</button>", "styles": ["a {color: red;}"], "styleUrls": [] } }, { "id": "yuv.custom.extension.quickfinder", "label": "yuv.custom.extension.home_yuuvis.description", "matchHook": "yuv-result|yuv-object", "plugin": { "component": "yuv-quickfinder", "inputs": { "autofocus": true, "allowedTargetTypes": ["appPersonalfile:pfpersonalfile"]}, "outputs": { "objectSelect": "(selection) => { parent.selection = selection; }"} } }, { "id": "yuv.custom.extension.content.preview.navigation", "label": "yuv.custom.extension.content.preview.navigation", "matchHook": "yuv-content-preview", "plugin": { "html": "() => { window._selectRow = (next) => { var el = api.util.$('.ag-center-cols-viewport .ag-row-selected')[next ? 'nextSibling' : 'previousSibling']; el && el.click();}; return '<div class=\"preview-navigation\"><button onclick=\"_selectRow()\">⇦</button><button onclick=\"_selectRow(true)\">⇨</button></div>';}", "styles": ["{ flex: 1; }", ".preview-navigation { display: flex; justify-content: center; margin: 0 4px; }", ".preview-navigation button { padding: 1px 4px; }"] } }, { "id": "yuv.custom.extension.search.filter.createdBy.me", "label": "yuv.custom.extension.search.filter.createdBy.me", "matchHook": "yuv-search-result", "plugin": { "component": "SearchFilterGroup", "inputs": { "skipCount": false, "skipTranslate": false, "query": [ { "lo": "OR", "filters": [ { "f": "system:createdBy", "o": "eq", "v1": "$CURRENT_USER$|eq" }, { "f": "system:creationDate", "o": "lte", "v1": "$TODAY$|lte" } ] } ] } } }, { "id": "yuv.custom.extension.search.filter.catalog.static", "label": "yuv.custom.extension.search.filter.catalog.static", "matchHook": "yuv-search-result", "plugin": { "component": "PluginSearchComponent", "inputs": { "init": "() => {return component.catalogToSelectableGroup('tenKolibri:strcatalogaddress');}" } } }, { "id": "yuv.custom.extension.search.filter.catalog.dynamic", "label": "yuv.custom.extension.search.filter.catalog.dynamic", "matchHook": "yuv-search-result", "plugin": { "component": "PluginSearchComponent", "inputs": { "init": "() => {return component.catalogToSelectableGroup('tenKolibri:strcataloggermancountries', component.loadCatalogOptions('tenKolibri:germancountries').then(countries => countries));}" } } }, { "id": "yuv.custom.extension.search.filter.catalog.all", "label": "yuv.custom.extension.search.filter.catalog.all", "matchHook": "yuv-search-result", "plugin": { "component": "PluginSearchComponent", "inputs": { "maxHeight": 300, "hideZeroCount": true, "init": "() => {return component.getAvailableCatalogs().map(f => component.catalogToSelectableGroup(f.id));}" } } }, { "id": "yuv.custom.extension.search.filter.date.future", "label": "yuv.custom.extension.search.filter.date.future", "matchHook": "yuv-search-result", "plugin": { "component": "PluginSearchComponent", "inputs": { "init": "() => {return component.toSelectable(component.extension, { array: [new SearchFilter('system:lastModificationDate', SearchFilter.OPERATOR.EQUAL, '$TODAY$,$TODAY$+27|eq')]});}" } } } ] }
Detailed descriptions of the properties can be found in the documentation (s. PluginExtensionConfig interface in framework library).
Plug-in Triggers
Form controls support users in entering data in formats such as strings, numbers, dates, booleans, or more specific ones such as e-mail addresses, users or references to other objects. If you need more such specific controls, you can extend the form controls as shown in the following example:
- paste the copied text to input via trigger (line 4)
- custom value picker for input via dialog (line 12)
- customize the reference component behavior to search for specific values (line 31)
- custom dynamic date for date filter (line 37)
{ "triggers": [ { "id": "yuv.custom.trigger.paste.clipboard", "label": "yuv.custom.trigger.paste.clipboard", "matchHook": "yuv-*", "icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" aria-hidden=\"true\" focusable=\"false\" width=\"1em\" height=\"1em\" preserveAspectRatio=\"xMidYMid meet\" viewBox=\"0 0 24 24\"><path opacity=\".3\" d=\"M17 7H7V4H5v16h14V4h-2z\" fill=\"white\"/><path d=\"M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1s-1-.45-1-1s.45-1 1-1zm7 18H5V4h2v3h10V4h2v16z\" fill=\"#626262\"/></svg>", "isExecutable": "(component) => component.parent.formControlName", "run": "(component) => {var _this = this; navigator.clipboard.readText().then((v) => v && _this.form.modelChange(component.parent.formControlName, {name: 'value', newValue: v}))}" }, { "id": "yuv.custom.trigger.quickfinder", "label": "yuv.custom.trigger.quickfinder", "matchHook": "yuv-*", "icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" aria-hidden=\"true\" focusable=\"false\" width=\"1em\" height=\"1em\" preserveAspectRatio=\"xMidYMid meet\" viewBox=\"0 0 24 24\"><path opacity=\".3\" d=\"M17 7H7V4H5v16h14V4h-2z\" fill=\"hotpink\"/><path d=\"M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1s-1-.45-1-1s.45-1 1-1zm7 18H5V4h2v3h10V4h2v16z\" fill=\"#626262\"/></svg>", "group": "visible", "buttons": {}, "plugin": { "component": "yuv-quickfinder", "inputs": { "autofocus": true, "allowedTargetTypes": ["appPersonalfile:pfpersonalfile"]}, "outputs": { "objectSelect": "(selection) => { parent.selection = selection; }"}, "popoverConfig": { "height": "30%", "width": "50%" } }, "isExecutable": "() => parent.formControlName === 'appClient:clienttitle'", "run": "() => { parent.selection = {}; trigger.openPopover().afterClosed().subscribe((result) => { result && api.form.modelChange('appClient:clientdescription', { name: 'value', newValue: parent.selection.description || '' }); });}" }, { "id": "yuv.custom.trigger.reference", "matchHook": "yuv-reference", "isExecutable": "() => parent.formControlName === null", "run": "() => { var ref = parent.childComponent; ref.descriptionField = 'system:objectId'; ref.allowedTargetTypes = ['appClient:minidoc']; ref.objectTypeFields = ['system:createdBy']; ref.searchFnc = (term) => { var ids = [api.form.getValue('appClient:clienttitle')]; ref.filters = ids && ids[0] ? [{'f':'appClient:clienttitle','o':'in','v1': ids}] : []; return Object.assign({term : '*{0}*'.replace('{0}', term)}, ref.queryJson);}; ref.objectSelect.subscribe(() => { var v = (parent.childComponent.innerValue || [])[0]; var val = (v && v.data && v.data.get('appClient:clienttitle')) || ''; api.form.setValue('appClient:clientdescription', val); });}" }, { "id": "yuv.custom.trigger.filter.variable.date", "label": "yuv.custom.trigger.filter.variable.date", "matchHook": "yuv-datetime-range", "group": "hidden", "isExecutable": "(component) => { return component.parent && component.parent.formControlName; }", "run": "(component) => { component.parent.dateVariables = [{offset: 3, value: '$TODAY$,$TODAY$+2'}].concat(component.parent.dateVariables); }" } ] }
Detailed descriptions of the properties can be found in the documentation (s. PluginTriggerConfig interface in framework library).
Plug-in Translations
In case you need to extend translations with new keys, please specify translations for each language in your system.
{ "translations": { "en": { "yuv.custom.action.home_yuuvis.label": "yuuvis Home", "yuv.custom.action.home_yuuvis.description": "yuuvis Description", "yuv.custom.action.home_yuuvis.simple": "yuuvis Home simple", "yuv.custom.action.home_yuuvis.component": "yuuvis Home component", "yuv.custom.action.home_yuuvis.component.full": "yuuvis Home component full", "yuv.custom.action.home_yuuvis.link": "yuuvis Home link", "yuv.custom.action.home_yuuvis.list": "yuuvis Home list", "yuv.custom.trigger.paste.clipboard": "Paste clipboard", "yuv.custom.trigger.paste.selection": "Paste selection", "yuv.custom.trigger.paste.suggestion": "Paste suggestion" }, "de": { "yuv.custom.action.home_yuuvis.label": "yuuvis Home DE", "yuv.custom.action.home_yuuvis.description": "yuuvis Description DE", "yuv.custom.action.home_yuuvis.simple": "yuuvis Home simple DE", "yuv.custom.action.home_yuuvis.component": "yuuvis Home component DE", "yuv.custom.action.home_yuuvis.component.full": "yuuvis Home component full DE", "yuv.custom.action.home_yuuvis.link": "yuuvis Home link DE", "yuv.custom.action.home_yuuvis.list": "yuuvis Home list DE", "yuv.custom.trigger.paste.clipboard": "Paste clipboard DE", "yuv.custom.trigger.paste.selection": "Paste selection DE", "yuv.custom.trigger.paste.suggestion": "Paste suggestion" } } }
Activating the Extension Feature and Importing Your Plug-in Configuration
To import a configuration file with your extensions (plug-ins), follow these steps:
- Open the client and open the settings.
- Click On in the Plug-in Configuration. This feature is offered to all users with the YUUVIS_SYSTEM_INTEGRATOR role.
- Click Import.
The file is uploaded to the CONFIGSERVICE so that all clients can use these extensions as well.
Removing the Configuration
Import an empty JSON file to remove the configuration.
Use Case Example
The following code block configures an action where a start form for a workflow process is offered.
{ "id": "yuv.custom.action.taskflow.id", "label": "yuv.custom.action.taskflow.label", "description": "yuv.custom.action.taskflow.description", "priority": 1, "icon": "<svg height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M0 0h24v24H0V0z\" fill=\"none\"></path> <path d=\"M4 10h12v2H4zM4 6h12v2H4zM4 14h8v2H4zM14 14v6l5-3z\"></path> </svg>", "group": "further", "range": "MULTI_SELECT", "isExecutable": "(item) => item.id", "buttons": { "finish": "yuv.custom.action.workflow.start" }, "plugin": { "component": "yuv-object-form", "inputs": { "__initOptions": "() => this.http.get(`/resources/config/taskflow-startform`, 'api-web').then((res) => {component.cmp.options = {formModel: res.data.tenant, disabled: false}})", "__init": "() =>parent.finished.subscribe((event) => {var selection = parent.selection;var cmp = component.cmp;var vars = [{ name: 'title', type: 'string', value: cmp.formData.title || '' },{ name: 'taskStatus', type: 'string', value: 'Open' },{ name: 'comment', type: 'string', value: cmp.formData.comment },{name: 'nextAssignee',type: 'string',value: cmp.formData.nextAssignee}];if (cmp.formData.expiryDatetime) {vars.push({name: 'expiryDatetime',type: 'date',value: cmp.formData.expiryDatetime,})}this.http.post('/bpm/processes',{businessKey: selection[0].id,name: selection[0].title || selection[0].id,processDefinitionKey: 'dms-lite-taskflow',attachments: selection.map((s) => s.id),subject: selection[0].title,variables: vars,},'api-web').then(() =>this.util.notifier.success(this.util.translate('yuv.custom.action.taskflow.success')))})" }, "outputs": { "statusChanged": "(status) => { parent.disabled = status.invalid; }" } } },
The configured action retrieves the taskflow-startform
form from the configuration resources. The framework component yuv-object-form
offers the form via the object action menu:
The form displays the localized fields corresponding to the Flowable process variables: title
(The task), expiryDatetime
(Due until), comment
(Comment for the next assignee) and nextAssignee
(Next assignee).
The taskStatus
variable that is internally used for process management is not displayed.
The expiryDatetime
variable can be saved only by calling the dms-lite-taskflow
process if a value is set. Thus, it is possible to prohibit setting a null
value for the datetime variable which is not allowed in Flowable.
Summary
We have shown how you can extend yuuvis® client as reference implementation or your custom client that is based on our core and framework libraries with custom elements.