Html Client Plugins

Html client plugins can be used to implement features not supported by origam and to supplement origam modeling where necessary using a piece of custom react code.
There are two kinds of plugins so far, both of which can use javascript/react to create a custom user interface. The section level plugin has access to screen section data and can be used to display them. The screen level plugin can change data fed into section level plugins by modifying the screen parameters.
We will demonstrate how to get started with the plugins on Audit screen from the Audit package which you can find it in the standard origam distribution. The hello world style plugins we will create here won’t do much but you can find a more complex implementation of the Audit view using plugins in the demo project https://github.com/origam/origam-demo.
Before you start creating a plugin make sure you have origam Root package version 5.1.8 or higher.

Plugin Placement

The plugins can be placed on any lazy loaded screen. To make a screen lazy loaded you have to specify a ListMethod, Method and ListSortSet in MenuItem.

Next you have to place the plugin (or a placeholder rather) on a screen. This is done in screen editor in Architect.

In the widget properties you have to specify the plugin’s SchemaItemName. Name the screen level plugin TestScreenPlugin and the section level plugin TestSectionPlugin. The section level plugin needs a DataMember so be sure to select one here. Also note that exactly one SectionLevelPlugin in the section must have the property AllowNavigation set to true. This is an equivalent of one screen section being a master and others details. So the AllowNavigation property would not be set to true if there were a master screen section here.

image

Custom Attributes

You can pass custom attributes to your plugin to make it configurable from the model (however we do not need the attributes in this demonstration). To do that you should create a new widget which will inherit from the widget you intended to use originally (ScreenLevelPlugin or SectionLevelPlugin). To create a new widget go to User Interface → Widgets → Basic Controls the right click the Basic Controls folder and choose New → Widget. This is what the TestScreenPlugin would look like if we wanted to create it as a separate widget.

When creating a new plugin widget be sure to set the ControlNamespace to Origam.Gui.Win, ControlToolBoxVisibility to FormDesigner and the ControlType to the ControlType of the plugin widget you are inhereting from.

And finally to define the attribute add a new property by right clicking on the Properties folder. You will see the property in the screen editor after you place the new plugin widget on it.

image

The property name and the value you specify in the form designer will be included among the xml attributes. You can read these attributes in the method initialize in the react plugin implementation. Note that the component might be rendered before the initialize method is run so you will probably have to store the attribute value in a state field like in the snippet below.

  @observable
  customAttribute = ""; 

  initialize(xmlAttributes: { [key: string]: string }): void {
    this.customAttribute = xmlAttributes["customAttribute"];
  }

Running the Client

To create and debug a plugin you have to compile the html client application. First clone repository GitHub - origam/origam: Enterprise software prototyping and development platform. somewhere on your machine and install latest version of Node.js.

Then open command line in directory origam\frontend-html and run:

npm install -g yarn

this will install the yarn package manager. Next run

yarn install

this will download and install all dependencies.

We will run the client application on a nodejs development server to make debugging simple. Before we do that we will also need an origam server to connect to and get the data from. You can use any running origam server you have access to: installed in IIS or in docker, local or remote. Just note the address and port the server is running on and write it to an environment variable WDS_PROXY_TARGET.

Now you can run the project with this command

yarn start

a development server should be started at https://localhost:3000/ when you go to that address you should see the client application.

Creating the Plugin

The client is written in typescript and react. You can use any javascript editor to edit the project like Visual Studio Code for example.

We will simplify our demonstration example by creating the plugins directly in the origam client source code. But in practise they should be placed in separate projects and published at npm repository as packages. More on this in later sections.

We will put the plugin files to origam\frontend-html\src\plugins\implementations starting with TestScreenPlugin.tsx. You can copy the following code into it:

import {
  ILocalization,
  ILocalizer,
  IScreenPluginData,
  IPluginDataView,
  IScreenPlugin 
} from "@origam/plugin-interfaces";

export default class TestScreenPlugin implements IScreenPlugin {
  $type_IScreenPlugin: 1 = 1;
  id: string = "TestScreenPlugin";

  requestSessionRefresh: (() => Promise<any>) | undefined;
  setScreenParameters: ((parameters: { [p: string]: string }) => void) | undefined;

  getComponent(data: IScreenPluginData, createLocalizer(localizations)): JSX.Element {
    return <div>TestPlugin!</div>;
  }

  onSessionRefreshed() {
  }

  initialize(xmlAttributes: { [p: string]: string }): void {
  }
}

As you can see the plugin implements interface IScreenPlugin and all its fields and methods.

The field $type_IScreenPlugin is not explicitly defined in the interface but it is needed to clearly show that the objects created from this class implement the interface.

requestSessionRefresh is a method you can call to refresh the screen (data)

setScreenParameters is a method you can use to set screen parameters

getComponent will be called when rendering the component and what you return here will be used to replace the placeholder widget you added to the screen in the Architect. The input to the method contains data which is model and database data you can use in your component. And the createLocalizer function wihich will create a Localizer more on that later.

onSessionRefreshed is called after the user pushes the Refresh button in the origam application. You can reload the data and update your components from here.

initialize is called after the application starts and the xmlAttributes parameter contains among other things the properties that you can model in architect and use them to configure the plugin.

Now we can create another file called TestSectionPlugin.tsx and put this into it:

import {
  ILocalization,
  ILocalizer,
  ISectionPluginData,
  IPluginDataView,
  ISectionPlugin
} from "@origam/plugin-interfaces";

export default class TestSectionPlugin implements ISectionPlugin {
  $type_ISectionPlugin: 1 = 1;
  id: string = "TestSectionPlugin ";

  getScreenParameters: (() => { [p: string]: string }) | undefined;

  getComponent(data: ISectionPluginData, , createLocalizer: (localizations: ILocalization[]) => ILocalizer): JSX.Element {
    return <div>{data.dataView.properties.map(prop => prop.id+", ")}</div>;
  }

  onSessionRefreshed() {
  }

  initialize(xmlAttributes: { [p: string]: string }): void {
  }
}

The TestSectionPlugin implements IsectionPlugin interface. As you can see this plugin cannot refresh the screen or set screen parameters but it can get the parameters and their values.

Finaly we have to register the plugins in the origam\frontend-html\src\plugins\tools\PluginRegistration.ts file so that the plugin classes can be recognized as plugins and found by the application:

export function registerPlugins(){
  registerPlugin("TestScreenPlugin", () => new TestScreenPlugin());
  registerPlugin("TestSectionPlugin", () => new TestSectionPlugin());
}

Now when we open audit view on any page:

image

We should see this:

Localization

to use localization features you have to create an instance of Localizer class by calling the createLocalizer function in the getComponenet method. The localizations array which is a parameter in the createLocalizer should be placed in a separate file next to the plugin file. The localization file for the TestScreenPlugin would be named TestScreenPluginLocalization.ts. The file should contain a constant named localizations in the following format:

export const localizations = [
  {
    locale: "de",
    translations: {
      day: "Tag",
    }
  },
  {
    locale: "cs-CZ",
    translations: {
      day: "Den",
    }
  },
  {
    locale: "en-US",
    translations: {
      day: "Day",
    }
  },
]

To localize a string inside your plugin call translate(key: string, parameters?: {[key: string]: any})
method of the localizer where the key is the key from the localization file and parameters is an object with named parameters. The localization strings are interpreted using library called messageformat. Look here to learn how you can parametrize your strings: Format Guide - messageformat a nice debug tool is also available here: Online ICU Message Editor

Plugin Deployment

In the simple demonstration we just completed we put all the plugin files into the client application source directly. This works as an example but the plugins should really be independent to avoid the need to maintain a fork of the client application and constantly merging changes from origam into it.

Publish Plugin as an npm package

A Good way of separating the plugin code from the client is to publish the plugin as an npm package and include it in the client application during a custom build.

Examples of such plugins can be found here:

There are a couple of origam npm packages you can use when developing a plugin:
@origam/plugin-interfaces we have already referenced this one in the example above. The package is needed in every plugin because it contains the necessary interfaces.

@origam/js-styles contains color definitions which you can reference in order to make your plugin compatible with the origam color style.

@origam/js-components contains simple reusable react components which again will make your plugin look more like the origam application. Especially the SimpleDropdown could be useful to you because the standard html input looks quite different from the origam dropdowns.

@origam/js-utils contains various functions useful in origam and potentially in the plugins.

A tutorial on how to create and publish an npm package can be found here: The complete guide to publishing a React package to npm - LogRocket Blog. Note that after you create the plugin package you can locally link it to the origam frontend project with npm link. This will make furhrer development of the package a lot easier. See details in the link above.

Alternative Approach to npm Packages

You may find that building the plugin package separately, publishing it on npm and including the package in the origam client creates some problems.

We had constant compatibility problems with one package in particular. The package build would work but when including it in the origam client build there were various build errors caused by different versions of the plugin package’s dependencies. Such problems can be solved by including the problematic dependency versions in the resolutions and peerDependencies sections of the package.json. But the problems were just too anoying.

So we decicded to simply clone the plugin repository into origam\frontend-html\src\plugins\implementations and register it as a git module. This way can both git repositories coexist and changes in either one are not confused with changes in the other one. The dependency problems during the build were also eliminated.

To registed the plugin repository as a git module create file origam/.gitmodules and put this inside:

[submodule "frontend-html/src/plugins/implementations/XYPlugin"]
	path = frontend-html/src/plugins/implementations/XYPlugin
	url = https://github.com/XYOrganization/XYPlugin.git

You also have to include the plugin’s delendencies in the origam client’s package.json and set the correct paths in origam\frontend-html\src\plugins\tools\PluginRegistration.ts.

Notes

If you think the plugin you are making might be used inside of TabControl, make sure to give the plugin componet’s tag this css style: flex-grow:1; If you don’t do that the plugin might not be hidden when it’s tab is deactivated leading to several tab contents to be rendered at the same time.

1 Like

Wouldn’t it be better to set this from the parent when creating a plugin instance? If it is mandatory, then we could just simply enforce it without the plugin developer having to think about it.

Sure we could do that, but:

  1. It could surprise some people when we override their css.
  2. If you don’t set flex-grow: 1 your plugin will still work fine in SplitPanel if you set some other css properties. width 100%...+ whatever was in the LineChart before + maybe some other stuff . When you put it in the TabControl one day will it work? Maybe it is better to keep in mind that you will have to use flex-grow to stretch it.
  3. Apparently it is possible to put more than one child into the Tab. I don’t know if it is advisable but if you do that you may want to set different flex-grow for the individual children (0, 1, 4…).

On the other hand it is css, so if you try hard enough you can override anything… but then you will make your plugin dependent on the client’s code more than you probably should.

1 Like

Ok, let’s see how it works. We can do this anytime as it would be a central change.

TestScreenPlugin should be implemented little bit differently in current version:

  1. There is missing required id property
  2. getComponent method fails to compile => this solves it: getComponent(data: IPluginData, createLocalizer: (localizations: ILocalization[]) => ILocalizer): JSX.Element (Taken from plugin’s examples: plugin-audit/AuditPlugin.tsx at master · origam/plugin-audit · GitHub)
import {
  ILocalization,
  ILocalizer,
  IPluginData,
  IPluginDataView,
  IScreenPlugin 
} from "@origam/plugin-interfaces";

export default class TestScreenPlugin implements IScreenPlugin {
  $type_IScreenPlugin: 1 = 1;
  id: string = "TestScreenPlugin";

  requestSessionRefresh: (() => Promise<any>) | undefined;
  setScreenParameters: ((parameters: { [p: string]: string }) => void) | undefined;

  getComponent(data: IPluginData, createLocalizer: (localizations: ILocalization[]) => ILocalizer): JSX.Element {
    return <div>TestPlugin!</div>;
  }

  initialize(xmlAttributes: { [p: string]: string }): void {
  }
}

I have updated the original post. Thanks.

A post was split to a new topic: Can’t import custom plugin to front-end