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 plugin-audit repository.
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 the Origam repository somewhere on your machine. Then follow the how to start guide to setup the Origam development environment.

Note that you don’t need to build and run the server fom the sources if you just want to develop the fronend plugins. 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 madify the proxy target nodes in the origam\frontend-html\vite.config.ts file.

Then you can run the project with this command at origam\frontend-html

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. First we will create a plugin boiler plate with a python tool which is included in the Origam repository. The files will be created in a directory you specify somewhere outside of the Origam repository. The idea is that the plugin files will have their own git repository here and you will copy them using the python tool to the Origam source when you want to build the Origam client application with the plugin. If you make changes to the plugin source files or the yarn.lock you use the python tool to copy the modified files back and then commit the changes to the plugin repository. Note that the python tool does not create the git repository that is up to you. The python tool can be found here:

"origam\plugins\pluginmanager.py"

First copy the tool template configuration origam\plugins\_pluginmanager_config.json file to origam\plugins\pluginmanager_config.json and remove the plugins section so that the file looks like this:

{
  "pathToOrigamRepo": "C:\\Repos\\origam",
  "plugins": { }
}

Also change the pathToOrigamRepo to the path where you cloned the Origam repository.

Then run the tool in any python 3 interpreter. You will only be presented with the 0 Create new plugin option since you have created no plugis yet. Type 0 and hit enter. Next type the plugin name TestSection and hit enter. Then you will be asked to input the path where the plugin files will be created. Choose some place on your drive outside of the Origam repository. For example C:\OrigamPlugins. The files will be created when you hit enter.

Open the plugin folder and look at the generated files. Here note the package.json file. The version string inside will be displayed when you click on your user name at the top right corner in the Origam client application and then click About. So be sure to update it when you make changes to your plugin. The depenedncies node should contain your plugin’s dependencies. These will be copied to the package.json in the Origam client application in the next step.

The tool is limited to creating one plugin. That means if you want to create another plugin with the tool the new plugin will live in a separate folder named after the plugin. But we want to place two plugins in the same folder. To do that we will add the other plugin file manulally. It also makes sence to rename the created directory from C:\OrigamPlugins\TestSection to C:\OrigamPlugins\TestPlugins since there will be two plugins in it. Go ahead and rename the folder and then open the config file origam\plugins\pluginmanager_config.json and rename the entry there as well. Don’t forget to change the paths in the config file to reflect the changed folder name.

Now we will create a new file at C:\OrigamPlugins\src\TestPlugins call it TestScreenPlugin.tsx and 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 modify the TestSectionPlugin.tsx file created earlier by the python tool. Put this into the file:

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 C:\OrigamPlugins\TestPlugins\PluginRegistration.ts file so that the plugin classes can be recognized as plugins and found by the application. This file was created by the python tool earlier but we have added another plugin so we have to update the file. Make sure the content of the file looks like this:

import { TestSectionPlugin } from "plugins/implementations/TestPlugins/src/TestSectionPlugin";
import { TestScreenPlugin } from "plugins/implementations/TestPlugins/src/TestScreenPlugin";
import { registerPlugin } from "plugins/tools/PluginLibrary";

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

Next run the python tool again. Now you will see more options. Select 1 Copy from plugin repository to Origam repository and choose your plugin name. This will copy the generated files to the origam repository and update dependencies in package.json. You can see what exactly changed in git.

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. A nice debug tool is also available here: Online ICU Message Editor

Whan Can You Reference From the Plugin

The interfaces you should be using when developoing the plugins are at origam\frontend-html\src\plugins\interfaces. Some abstract classes implementing the plugin ineterfaces are at origam\frontend-html\src\plugins'components. If you reference anything else from the Origam code you may find that your dependency has changed and your code is broken the next time you build the client with your plugin. So be careful about that.

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/src/AuditPlugin/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