Adapt a SAIL Recipe to Work with My Applications

In this article, we will cover how you can take a SAIL recipe and modify it to work with a different interfaces and application objects.

While SAIL recipes are a great resource to help designers explore and learn design patterns, they are also frequently used to add functionality. Getting a SAIL recipe to work in an interface is as simple as copying and pasting the expression into the interface designer. However, using SAIL recipes in your application, with your application data isn’t always that easy. This is because most SAIL recipes are designed to work with a very particular data type, the employee data type from the Use the Write to Data Store Entity Smart Service Function on an Interface recipe. In some recipes, the employee data type is not used, but the same steps apply when converting non-employee recipes.

When modifying the data used in a SAIL recipe, you will encounter two ways data is represented in the expression:

  • Hard-coded: typically seen on recipes that editable form fields have data hard-coded. These values are stored as a local variable.
  • Queried: typically seen on recipes that display charts or paging grids. These values are brought in via a!queryEntity() and stored in a local variable

We will walkthrough both hard-coded and query recipe examples, converting the SAIL recipes to meet a purchase request use case. In both these examples, our purchase request use case has a flat data structure. In addition to these examples, we will also discuss modifying a recipe to fit a nested data structure.

Modifying the Recipe to use my Data Instead of Hard-Coded Data

The add, edit, and delete data in an inline editable grid recipe shows an example expression where hard-coded data is used to provide pre-poulated fields in the editable grid. Because an editable grid is most likely to be found within a form interface, we are going to walk-through taking the hard-coded employee data and converting it to an purchase request interface that will eventually be integrated with a process model.

images:SAIL Recipe Editable Grid

Before we continue, it’s important to understand what is meant when we refer to a "form". A form, is an interface that’s used to capture information in one or more fields, and that information gathering is usually a part of a process/workflow. This means, when you think of a form, do so with the premise that the user expects that the information they add is not immediate, and there is some sort of confirmation/submit action associated with the formalization of that information. SAIL is flexible enough to build an editable grid in a report or record, which could be used to write data, through smart service functions. However, for this use case, we’re sticking with the more traditional form that will hook up to a process.

"Employee" data is represented in this recipes as a hard-coded array of dictionary data stored in a local variable:

local!employees: {
      { id: 1, firstName: "John" , lastName: "Smith" , department: "Engineering" , title: "Director" , phoneNumber: "555-123-4567" , startDate: today()-360 },
      { id: 2, firstName: "Michael" , lastName: "Johnson" , department: "Finance" , title: "Analyst" , phoneNumber: "555-987-6543" , startDate: today()-360 },
      { id: 3, firstName: "Mary", lastName: "Reed" , department: "Engineering" , title: "Software Engineer" , phoneNumber: "555-456-0123" , startDate: today()-240 },
  },

This hard-coded data is similar to what would be found in a CDTs. For our purchase request CDT, let’s assume the following data structure:

  • id (number (integer)) - primary key value to persist data to a relational database
  • summary (text) - Describes the purchase request, for example, the item name
  • qty (number (integer)) - how many of that particular item
  • unitPrice (number (decimal)) - how much each item costs
  • dept (text) - which department needs the particular item
  • due (date) - when this item needs to be received by

Once we have a CDT, we can start modifying the recipe by adding a rule input, and making it a datatype of a CDT array. A rule input is used instead of a local variable because this editable grid will be passing data into a Process.

images:Adapting_SAIL_Recipe_Grid_Rule_Input.png

Next, we'll remove all of the hard-coded data in local!employees by deleting the entire local variable. Expectedly, we will get an error message:

This is ok, because it’s a way to prompt us that we need to replace all remaining uses of local!employees in our expression with the newly created rule input. Ctrl+H/Cmd+H is an extremely useful keyboard shortcut that will do this in one step.

images:Adapting_SAIL_Recipe_CTRL_H.png

There are a number of keyboard shortcuts available in expression editors, these can be found by hovering over the question mark at the top right of an expression editor or on the expressions page of the docs.

After we replace all the references to local!employee, we’ll see our form again, but it’s still configured to collect employee information. We'll still need to update the individual SAIL components to meet our use case.

Making Sure the SAIL Components Work with My Data

Unlike going from a hard-coded local variable to a rule input, the steps required to modify the SAIL components are going to be unique for each recipe and each use case. Instead of a step-by-step guide explaining what needs to be done, we'll address the high level things that were changes

Modifying the Grid Columns

The number of columns changed as well as the column header labels:

images:Adapting_SAIL_Recipe_Column1.png images:Adapting_SAIL_Recipe_Column2.png

Change the SAIL Components

You can think of an editable grid row as its own independent form. Because of these, the SAIL components used to populate the employee grid need to change

For example, the first name column in Employee:

a!textField(
  label: "first name " & fv!index,
  value: fv!item.firstName,
  saveInto: fv!item.firstName,
  required: true
)

changed to the summary column in our updated editable grid:

a!textField(
  label: "Summary " & fv!index,
  value: fv!item.summary,
  saveInto: fv!item.summary,
  required: true
)

Clean-up the Remaining Recipe

The last part of our update included final formatting and expression cleanup. This included:

  • Changing the editable grid's addRowlink parameter
  • Updating the form and submit button's label
  • Removing the load() function since there were no remaining local variables

Again, these steps will be unique for every recipe modification. But the high-level steps will be similar for any recipe you are trying to modify. We are left with an Interface that looks like this:

images:Adapting_SAIL_Recipe_Final_Grid.png

From this point, the recipe can be used in a process either through a start form, or in an attended task later in a workflow. While the configuration of other recipes to work with your process with differ, the top level steps of, (1) replacing hardcoded data with rule inputs, (2) making sure the SAIL components are still relevant for your need, and (3) final formatting and clean up will remain the same.

Modifying the Recipe to Use my Data Instead of Queried Data

So what happens when we try to convert a recipe, but instead of dealing with hard-coded data, we’re dealing with data from a query, either via queryrecord() or a!queryEntity(). These types of examples are usually found in recipes that make up the dashboard of a report or are referenced in a view of a record.

Let's continue with the purchase request use case and assume that our requested items are being stored in a database table. Our users would like a report that shows them the number of requested items per department. Additionally, they would like to see details about each department’s requests.

images:Adapting_SAIL_Recipe_Chart_Start.png

The Filter the Data in a Grid Using a Chart recipe is perfect for this. When replacing hard-coded data, we simply removed the local variable recipe and replaced with a rule input. Here, we need to modify to the datasubset local variables to query our database table instead of the recipe's employee table.

There are four places we need to update local variable references: local!chartPagingInfo, local!gridPagingInfo, local!chartDataSubset, and local!gridDatasubset.

Changing Aggregated Query Data

For our datasubset for our chart, local!chartDatasubset was this:

local!chartDatasubset: a!queryEntity(
  entity: cons!EMPLOYEE_ENTITY,
  query: a!query(
    aggregation: a!queryAggregation(aggregationColumns: {
      a!queryAggregationColumn(field: "department", isGrouping: true),
      a!queryAggregationColumn(field: "id", aggregationFunction: "COUNT"),
    }),
    pagingInfo: local!chartPagingInfo
  )
)

We will modify it to look like this:

local!chartDatasubset: a!queryEntity(
  entity: cons!PURCHASE_REQUEST_ITEMS_ENTITY,
  query: a!query(
    aggregation: a!queryAggregation(aggregationColumns: {
      a!queryAggregationColumn(field: "dept", isGrouping: true),
      a!queryAggregationColumn(field: "id", aggregationFunction: "COUNT"),
    }),
    pagingInfo: local!chartPagingInfo
  )
)

a!chartPagingInfo will also need updated. Let’s replace the sorting from department to id.

Changing Selected Query Data

Now it’s on to local!gridDatasubset where this:

local!gridDatasubset: a!queryEntity(
  entity: cons!EMPLOYEE_ENTITY,
  query: a!query(
    selection: a!querySelection(
      columns: {
        a!queryColumn( field: "firstName"),
        a!queryColumn( field: "lastName"),
        a!queryColumn( field: "department"),
        a!queryColumn( field: "title")
       }
     ),
    filter: if(
      isnull( local!selectedDepartment ),
      null,
      a!queryFilter( field: "department", operator: "=", value: local!selectedDepartment )
    ),
    pagingInfo: local!gridPagingInfo
  )
)

Will get modified to query our data properly:

local!gridDatasubset: a!queryEntity(
  entity: cons!PURCHASE_REQUEST_ITEMS_ENTITY,
  query: a!query(
    selection: a!querySelection(
      columns: {
        a!queryColumn( field: "summary"),
        a!queryColumn( field: "qty"),
        a!queryColumn( field: "unitPrice")
      }
    ),
    filter: if(
      isnull( local!selectedDepartment),
      null,
      a!queryFilter( field: "dept", operator: "=", value: local!selectedDepartment
      )
    ),
    pagingInfo: local!gridPagingInfo
  )
)

Again, we’ll need to adjust paging info, so a!gridPagingInfo sorting will change from title to id.

Making Sure the SAIL Components Work with My Data

Once we’ve got these local variables querying our data, we'll need to change the SAIL Components so they work with our new queried data. For our purchase request use case this included:

  • Changing the pie chart's chartSeries to refer to item data
  • Changing the grid's text columns to refer to item data
  • Formatting the link and pie chart labels

From this, we end up with a chart that filters our purchase requests:

images:Adapting_SAIL_Recipe_ChartFinal.png

Dealing with Nested Values

In our purchase request example, we’ve gone from one flat relatively simple data type to another. This will not always be the case. What should you do if you if you want to adapt a SAIL recipe to a CDT value that’s nested?

Let’s say that our employee CDT was nested, with department and title living in a nested position CDT. When we go to query the data our expression would look something like this:

a!querySelection(columns: {
   a!queryColumn(field: "firstName"),
   a!queryColumn(field: "lastName"),
   a!queryColumn(field: "position.department"),
   a!queryColumn(field: "position.title")
  }
)

The alias parameter in a!queryColumn() can be used to make referring to nested values easier in a SAIL expression.

We would follow a similar convention when querying process a process backed record and wanted to reference a process property:

a!queryAggregation(
  aggregationColumns: {
   a!queryAggregationColumn(field: "department", isGrouping: true),
   a!queryAggregationColumn(field: "pp.id", aggregationFunction: "COUNT"),
  }
)

The tricky part when trying to adapt a CDT with nested values comes when we want to display that data. It’s important to understand that the query brings back both the value and the data structure. This means that when I’m referencing a nested query column, I need to account for my data structure when using index(). For things like paging grids, my gridTextCoumn looks like this:

a!gridTextColumn(
  label: "Department",
  field: "position.department",
  data: index(local!datasubset.data.position, "department", {})
)

For chart data, my chartSeries would look like this:

a!chartSeries(
 label: index( local!datasubset.data, "department", null),
 data: index( local!datasubset.data.pp, "id", null) 
)
17.2

On This Page

FEEDBACK