Karma Forms
Creating workflows based on the Karma Form Engine
Creating a workflow based on the Karma Form Engine basically follows the same rules as creating a usual IDM workflow with two expceptions that will be explained in this document.
- Request and approval forms contain exactly two form fields:
k5_wf_fields(containing the form configuration as JSON) andk5_wf_model(containing the form model as JSON). See Form configuration - The data item mappings for the two form fields do not contain single values but structed information: the JSON form configuration for
k5_wf_fieldsand a structured model fork5_wf_model. See Data item mapping
Form configuration
For both, request and approval forms, configure two form fields as follows. The availability of those fields will instruct Karma to use the Karma Form Engine rather than render the default form.
| Form Field Name | Data Type | Control Type |
|---|---|---|
k5_wf_fields | string | TextArea |
k5_wf_model | string | TextArea |
Data item mapping
This section describes how to correctly pass data to Karma forms.
k5_wf_fields
The k5_wf_fields must contain a valid JSON configuration (JSON5 is supported as well) conforming with the Angular Formly documentation as the form engine is based on Angular Formly. An example on how the form configuration must look like can be found here (See see Fields section in Output).
The form configration can be passed either directly by the data item mapping configuration or by using a separate form configuration object:
Directly passing the form configuration
In the data item mapping for k5_wf_fields, directly return the config as a JSON string, for example:
ScriptVault.JSON.stringify([
{
"key": "firstName",
"type": "input",
"templateOptions": {
"label": "First Name"
}
},
{
"key": "lastName",
"type": "input",
"templateOptions": {
"label": "Last Name",
"pattern": "([0-9a-zA-Z]{2,})"
}
},
{
"key": "displayName",
"type": "input",
"templateOptions": {
"label": "Display Name",
"disabled": "disabled"
}
}
])
Configuring the form input in a separate configuration object
If you plan to re-use the form, you might want to store it in a separate configuration object. To do that create a new object in designer:
In outline view: right click the user application driver, select New -> Rescource..., set a name (for example: 'form-my-workflow') and choose text/+text as the type. Then configure the form in that object.
In the data item input mapping for k5_wf_model, insert the following code (adjust the dn as needed) to instruct Karma to use the form configuarion.
ScriptVault.k5 !== true ? '{}' : IDVault.get('CN=form-my-workflow,cn=UserApplication,cn=DriverSet,o=system', 'Form', 'DirXML-Data');
Configuring the form input in a separate configuration file (the preferred way)
Instead of defining your form in LDAP (as in the example above), you can just put a JSON definition file inside your config folder and reference it as shown in the following snippet.
ScriptVault.k5 !== true ? '{}' : ScriptVault.read('file:forms/my-form/fields.json')
k5_wf_model
Directly passing the model configuration
The following shows a valid data item input mapping for k5_wf_model. The code will be evaluated by the User Application and Karma. Therefore the environment should be determined by evaluating this expression: ScriptVault.k5 !== true. If it evaluates to true, the code will be be run inside the User Application, otherwise it will be run in Karma.
ScriptVault.k5 !== true ?
// This part will be evaluated by the user application, it can be a JSON string or a function returning a JSON string
// The result JSON will be available the Karma as `flowdata`
(function() {
return ScriptVault.JSON.stringify({
firstName: "Max",
lastName: "Master"
})
})()
:
// This part will be evaluated in Karma, you can access the attributes given by the User Application code by requesting the `flowdata` object
(function() {
return JSON.stringify({
firstName: flowdata.firstName,
lastName: flowdata.lastName,
displayName: `${flowdata.firstName} ${flowdata.lastName}`
})
})()
Configuring the model input in a separate configuration object
As with the form configuration, the model can be extracted to a separate form object. This approach is highly recommended because it supports an optimized development process as described in the section Form development.
To create that object in Designer, right click the user application driver, select New -> ECMAScript... then set a name (e.g. my-model-script). The configure the input item mapping of your model (k5_wf_model) to use that script:
ScriptVault.k5 !== true ? '{}' : ScriptVault.runScript('cn=my-model-script,cn=UserApplication,cn=DriverSet,o=system')
In your script my-model-script, you have to export a javascript module that returns your model, for example:
// this example loads information for the wf recipient and returns that information for usage in the model
'use strict'
module.exports = async ({ IDVault, recipient }) => {
const user = await IDVault.get(recipient, 'user', ['givenName', 'sn'])
return {
firstName: user.firstName,
lastName: user.lastName,
displayName: `${user.firstName} ${user.lastName}`
}
}
Configuring the model input in a separate configuration file (the preferred way)
Instead of defining your script in LDAP (as in the example above), you can just put a script file inside your config folder and reference it as shown in the following snippet.
ScriptVault.k5 !== true ? '{}' : ScriptVault.runScript('file:forms/my-form/my-model-script.js')
Defining custom scripts
You are able to write your own custom javascript functions to bypass the drawback that formly expressions within the json field configuration do not allow function definition. Custom functions are added as a model property and therefore added within the model script (see above on how to define), either inline or by loading an external script file.
Inline script
'use strict'
module.exports = async () => {
return {
disabled: false,
cn: 'testix',
givenName: 'Test',
surname: 'Ix',
$scripts: `
exports.isDisabled = model => !!model.disabled
`
}
}
External script file
// my-model-script.js
'use strict'
module.exports = async () => {
return {
disabled: false,
cn: 'testix',
givenName: 'Test',
surname: 'Ix',
$scripts: await ScriptVault.readScript('file:forms/create-user/scripts.js')
}
}
// scripts.js
exports.isDisabled = model => {
console.log('model', model)
return !!model.disabled
}
Usage in formly expression
Within a formly expression, just call your function prefixed with $.
[
{
"type": "input",
"key": "givenName",
"className": "col-sm-12",
"templateOptions": {
"label": "Given Name"
},
"expressionProperties": {
"templateOptions.disabled": "$.isDisabled(model)"
}
}
]
Available objects in Karma sandbox
The code to fill the form model will be run in Karma inside a sandbox. In this sandbox, the following objects will be available:
initiator: the DN of the workflow initiatorrecipient: the DN of the workflow recipientflowdata: an object containing data passed in the User Application section of the data item input mapping fork5_wf_modelconsole: allows debugging withconsole.log()(the result can be seen in the Karma server log)IDVault: the Karma implementation of User ApplicationsIDVaultIDVault.get(dn, objectType, [attr1, attr2, attr3]): Returns an object of the requesteddncontaining all defined attributes (this function is async: useasync,await)IDVault.globalQuery(null, 'dal-query-name', {param1: value1}): Executes the specified Karma DAL query with the specified parameters (this function is async: useasync,await)
ScriptVaultthe Karma implementation of User ApplicationsScriptVault_: lodash
Many ECMAScript 6 features are available as well, including but not limited to:
- Arrow Functions
- Async Functions
- Block-scoped Variable let
- Block-scoped Variable const
- Template Strings
- Desctructuring (Karma v2.8.10 onwards)
Form development
Karma provides a form-designer to quickly develop forms: https://karma.mycompany.com/form-designer. The main advantage of using that designer is that you instantly see the result of your configuration, no NetIQ Designer deployment needed.
Create a new form using form designer, follow those steps:
- create a folder (e.g.
my-form) inside the Karma configuration directory: e.g.karma/config/forms - Inside
my-formcreate the following files:
config.json (or config.json5): contains possbile form actions, e.g.:
{
"actions": ["approve", "deny", "refuse"]
}
fields.json (or fields.json5): contains the Angular Formly form configuration, e.g.:
[
{
"key": "firstName",
"type": "input",
"templateOptions": {
"label": "First Name"
}
},
{
"key": "lastName",
"type": "input",
"templateOptions": {
"label": "Last Name"
}
}
]
model.json (or model.json5): contains a mocked input model, e.g.:
{
"firstName": "Max",
"lastName": "Master",
}
formState.json (or formState.json5): contains workflow initiator and recipient, e.g.:
{
"recipient": {
"entryDN": "cn=kjell,ou=user,o=data"
},
"initiator": {
"entryDN": "cn=mmaster,ou=user,o=data"
}
}
From Karma v2.8.9 onwards you are also able to develop your data item input mapping for the form model (k5_wf_model). To accomplish that, you need to do the following:
Create a model.js inside your karma/config/forms/my-form. This file will be prefered over model.json. Inside the file, load your custom script as you would do for a production setup:
ScriptVault.k5 !== true ? '{}' : ScriptVault.runScript('cn=my-model-script,cn=UserApplication,cn=DriverSet,o=system')
Note: You can mock flowdata by providing a flowdata.json or flowdata.js. Setting flowdata items in the User Application part (if ScriptVault.k5 is not true) in the previous script will not work because the script will not be executed by the User Application (this effects only the form designer)
In your Karma configuration (e.g. local.yaml), setup a mapping to load the model script (cn=my-model-script,cn=UserApplication,cn=DriverSet,o=system) from a file instead of eDirectory:
# ...
runInContext:
scripts:
'cn=my-model-script,cn=UserApplication,cn=DriverSet,o=system': './forms/my-form/my-model-script.js'
# ...
Now you can create the mapped file inside karma/config/forms/my-form and develop your data item input mapping. When you're done, just remove the scripts mapping in local.yaml so that the script will be loaded from eDirectory again, if desired.
my-model-script.js:
// this example loads information for the wf recipient and returns that information for usage in the model
'use strict'
module.exports = async ({ IDVault, recipient }) => {
const user = await IDVault.get(recipient, 'user', ['givenName', 'sn'])
return {
firstName: user.firstName,
lastName: user.lastName,
displayName: `${user.firstName} ${user.lastName}`
}
}
After a refresh in form designer you'll see the results of your script instantly.
Examples
Edit user
The following example shows how to create a simple user edit form. It basically uses a k5-select to search for users using a Karma DAL query and updates other form fields based on the result by triggering and onChange event within the 'templateOptions'.
Form Configuration
[
{
"type": "k5-select",
"key": "user",
"className": "col-sm-12",
"templateOptions": {
"required": true,
"selectFirst": false,
"label": "Please select a direct user",
"noChoiceLabel": "No user selected",
"dal": {
"key": "user_by_name_or_cn",
"async": true,
"options": {
"$take": 5
}
},
"valueProp": "entryDN",
"labelExpression": "($item.fullName || $item.sn)",
"descriptionExpression": "$item.entryDN",
"onChange": "_.assign(model, _.pick((model.user && _.find(to.options, {entryDN: model.user})) || { givenName: '', sn: '' }, 'givenName', 'sn'))"
}
},
{
"type": "input",
"key": "givenName",
"className": "col-sm-6",
"templateOptions": {
"label": "Given Name",
"required": false
}
},
{
"type": "input",
"key": "sn",
"className": "col-sm-6",
"templateOptions": {
"label": "Given Name",
"required": false
}
}
]
DAL configuration
dal:
'user_by_name_or_cn':
type: 'ldap:list'
options:
base: '<%= users.base %>'
scope: '<%= users.scope %>'
filter: '<%= users.filter %>'
attributes:
- givenName sn
- entryDN
- displayName fullName
sortBy: '<%= users.sort %>'
# qAttributes defines in which LDAP attributes should be for the k5-select input
qAttributes: givenName sn cn