workflow:scripts

Scripts

The Pyrus scripting platform lets users expand the functionality of Pyrus forms to do things like:

  • Automatically calculate and fill in field values based on data entered in other fields.
  • Validate field values.
  • Automatically fill in fields using the form register.

The scripts are written in JavaScript and executed within Pyrus forms. The platform provides a trusted environment to run these scripts and controls their access to your data.

Getting started

To try out Pyrus scripts, follow these steps:

  1. Create a form that contains the fields Start date, End date and Duration. This might be a request form for a business trip or vacation.
  2. Open the form’s settings and click on Edit script.
  3. Insert the following code into the script editor:
      form.onChange(['Start date', 'End date'])
      .setValue('Duration', state => {
        const startDate = state.changes[0].date;
        const endDate = state.changes[1].date;
      
        if (!startDate || !endDate)
          return [null];
            
        const diff = daysBetween(new Date(startDate), new Date(endDate));
        return [diff + 1];
      });
    
    function daysBetween(d1, d2) {
      const msInDay = 1000 * 60 * 60 * 24;
      return Math.floor((d2.getTime() - d1.getTime()) / msInDay);
    }
      
  4. Save your changes.
  5. Open a new business trip or vacation request form, then fill in the Start date and End date fields. The Duration field will automatically show the number of days requested:

Architecture

These scripts are pieces of JavaScript code that monitor field value changes, and define the logic for calculating values in dependent fields.

Imagine an invoice form with the fields Price, Quantity, and Total. To automatically calculate the total and display this value in the Total field, use this piece of code:

form
  .onChange(['Price', 'Quantity'])
  .setValue('Total', state => {
      const price = state.changes[0].value;
      const quantity = state.changes[1].value;

      return [
        price * quantity
  });

Let’s take a closer look. The script has a global FormProxy object called form, and it insures that the code interacts with a Pyrus form:

interface FormProxy {
  onChange(fieldNames: string[]): ChangeHandler;
  
  fetchSelfRegister(
    filterFn: (filter: RegisterFilter) => RegisterFilter,
    fieldNames: string[]
  ): Promise;
}

The onChange method takes the names of the fields that are going to change (Price and Quantity) as its first argument, then returns the ChangeHandler object.

You will find more information about the fetchSelfRegister method in Form register.

ChangeHandler defines the logic for calculating the value of the dependent field (Total), and it executes whenever a change occurs in any fields described in the onChange method.

interface ChangeHandler {
  setValue(
    fieldName: string, 
    calcFunction: (state: FormState) => CompositeValue
  ): void;
  
  setValues(
    fieldNames: string[], 
    calcFunction: (state: FormState) => CompositeValue[]
  ): void;
  
  setValueAsync(
    fieldName: string, 
    calcFunction: (state: FormState) => Promise
  ): void;
  
  setValuesAsync(
    fieldNames: string[], 
    calcFunction: (state: FormState) => Promise
  ): void;  
  
  validate(
    fieldName: string, 
    validateFunction: (state: FormState) => {errorMessage: string} | null
  ): void;
  
  validateAsync(
    fieldName: string, 
    validateFunction: (state: FormState) => Promise<{errorMessage: string} | null>
  ): void;  
}

The setValue method’s arguments are:

  • fieldNames — a list of field names whose values need to be calculated.
  • calcFunction — a function responsible for field value calculation.

calcFunction receives a FormState object as its only argument, then returns a new value (see Field value formats) for the calculated field described in the setValue method.

When you need to calculate the values of several fields based on the same data, use the setValues method. It takes an array of field names, while calcFunction returns an array of these field’s values.

If calculations require asynchronous operations (like waiting for the completion of the register request), use the asynchronous versions of the methods: setValueAsync and setValuesAsync. In this case, a Promise object, which will return the field values, is returned to calcFunction. You will find an example of how this can be used in Form register.

The validate and validateAsync methods allow for flexible field validation and error notifications. For more details, see Field validation.

FormState receives the values for Price and Quantity and uses them to calculate Total.

interface FormState {
  changes: FieldValue[];
  prev: FieldValue[];
}

The “changes” field contains an array of onChange’s current field values. The “prev” field contains an array of the calculable fields’ previous values.

Tables

Scripts can be used in forms that contain tables. Use them to:

  • calculate table field values based on the values of cells in the same row.
  • reference the overall sum of the values in a table column to calculate the value of a non-table field.

Unlike automatically calculated table fields, scripts allow you to define conditions and support other field types, like multiple choice.

Creating a calculable table field

Suppose you have a form that contains a table with a list of items available for purchase. Each item has a Name, Price, Quantity, Subtotal, and Tax field. Tax is a multiple choice field with three options: 0%, 5% and 7%.

The value of the Subtotal field can be automatically calculated and filled in with help from this piece of code:

form.onChange(['Price', 'Quantity', 'Tax'])
  .setValue('Subtotal', state => {
    const [price, quantity, taxRate] = state.changes;
    
    if (!price.value || !quantity.value)
      return [0];

    let cost = price.value * quantity.value;

    const tax = taxRate.choice_name 
      ? parseInt(taxRate.choice_name) 
      : 0;

    if (taxRate.choice_name)
      cost += cost * (tax / 100);

    return [cost];
  });

Creating a calculable column

Let’s place two fields, Discount and Grand total, at the bottom of the table.

To automatically calculate the Grand total (the total after the discount), use this code:

form.onChange(['Subtotal', 'Discount'])
  .setValue('Grand total', state => {
    const [cost, discount] = state.changes;

    const total = cost.sum * (1 - discount.value / 100);

    return [total];
  });
  

Both pieces of code can work together, creating a chain of dependent fields.

Validation

Scripts allow you to validate field values and avoid errors when filling out forms.

For example, we’d like to automatically check whether dates are entered correctly into a business trip form. We talked about this form earlier in the article. Let’s configure the automatic date check by adding the following code to the form’s script:

form.onChange(['Start Date', 'End Date'])
    .validate('End Date', state => {
        const [start, end] = state.changes;

        if (start.date && end.date && start.date >= end.date)
            return {errorMessage: 'Can’t be earlier than start date'};

        return null;
    });
  

Now, if we accidentally put in an end date that’s earlier than the start date, the form will return an error message.

Form register

Scripts also allow you to use the form register in your calculations.

Imagine a report form that has three fields: Year, Number and Document (which is used to attach the report file). The script below allows you to do a duplicate check. It accesses data from the form register and Pyrus lets you know if the report has been released before.

form.onChange(['Year', 'Report No.'])
    .validateAsync('Report No.', async state => {
        const [year, num] = state.changes;

        if (year.text && num.text) {
            const duplicates = await form.fetchSelfRegister(f => f.fieldEquals('Year', year).fieldEquals('Report No.', num), []);
            const firstDuplicate = duplicates.tasks[0];
            if (firstDuplicate)
                return {errorMessage: `Report has already been released`};
        }

        return null;
    });
  

If we now try to fill out the form with the data from an existing report, we’ll see an error message and a link to the existing report.

Limitations

User scripts are executed in an isolated context with a basic JavaScript environment, excluding global context (window, global) and network interaction (XMLHttpRequest and fetch).

Currently, you can only use scripts to fill out and edit forms in the web interface.

Timeouts

Script launching and field value calculation time out after five seconds. If your code runs longer than that, the script will be stopped.

Dependencies

  • Circular dependencies (e.g. field A depends on field B while B also depends on A) aren’t allowed. These scripts won’t launch.
  • A table field can only reference fields in the same table.
  • When table and non-table fields are combined, a non-table field cannot reference the table’s individual cells. It only has access to the sum of all the values in a column.

Debugging

You can use the console.log(…args) method to debug your code. This method allows you to display the variables and debugging information in your browser console. The method takes primitives like String, Number, and Boolean (as well as objects without cyclical dependencies) as arguments.

Note that changing a script will affect all the users working with the form, as there’s currently no testing environment available. You can create a copy of the existing form template and do your testing there so that it doesn’t interfere with employee activities.

Field value formats

Form field values can be represented by an object, a string, or a number. This applies both to reading (obtaining values via FormState) and writing (returning a new field value).

Type Reading Writing
Text,
Email,
Telephone
{
  text: "value"
}
{
  text: "new value"
}
or
"new value"
Number,
Money
{
  value: 10
}
or (in the case of table fields)
{
  value: 10,
  sum: 50
}
{
  value: 42
}
or
42
Multiple choice
{
  choice_id: 1,
  choice_name: "Yes"
}
{
  choice_id: 1
}
or
{
  choice_name: "Yes"
}
Due date
{
  date: "2020-01-01"
}
{
  date: "2020-01-05"
}
or
{
  days_from_create: 10
}
Due date and time
{
  date_time: "2020-01-01T10:30:00Z"
}
{
  date_time: "2020-01-05T10:30:00Z"
}
or
{
  hours_from_create: 10
}
Catalog
{
  item_id: 123,
  columns: {
    "column name": "value"
  }
}

Was this article helpful?

Yes, thanks! No, I have a question