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:

Tenant-specific:

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.

Example for adding a new state to the client
{
"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).

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.

Example for adding a new item to the main menu
{
  "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.

Example for adding a new state to the client
{
	"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.

Example for adding a new action to the object actions menu
{
  "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)

Example for adding a new aspect to the object details
{
"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 and TODAY (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,TODAY+27) (line 103)


Example for adding a new aspect to the object details
{
"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)
Example for adding a custom functionality to the form field controls
{
"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.

Example for adding a custom functionality to the form field controls
{
  "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.

Read on

CLIENT Service 

Service providing yuuvis® Momentum client as reference implementation. The client enables the work with documents and metadata (Document Management System) and provides a basis for further modeled business applications. Keep reading

Custom Client Build with Libraries

Documentation of the client core and framework libraries, as usable for custom client development. Keep reading

Client Scripting API

This article describes the Client Scripting API used to develop custom clients as well as client-side form scripts. For more information on building custom clients, refer to the client API documentationKeep reading