import re
regex = re.compile(r"(category: sailrecipe\n\---)(\n\n\#\#\# Goal \#\#\#\n)(.*?)(\n\n)", flags=re.MULTILINE | re.IGNORECASE)
test_str = ("\n\n"
"---\n"
"layout: basic\n"
"title: Configure a Button that Skips Validation (a Cancel Button) \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Display a button that submits the form even if the form contains validation errors such as a blank required field or an invalid text.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!a,\n"
" local!b: \"abc@\",\n"
" local!cancel: false,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Cancel Button\",\n"
" firstColumnContents: {\n"
" a!textField(\n"
" label: \"Text 1\",\n"
" value: local!a,\n"
" saveInto: local!a,\n"
" required: true\n"
" ),\n"
" a!textField(\n"
" label: \"Text 2\",\n"
" instructions: \"@ is an invalid character\",\n"
" value: local!b,\n"
" saveInto: local!b,\n"
" validations: if(find(\"@\", local!b)=0, null, \"Invalid character!\")\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\",\n"
" style: \"PRIMARY\"\n"
" ),\n"
" secondaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Cancel\",\n"
" confirmMessage: \"Are you sure?\",\n"
" value: true,\n"
" saveInto: local!cancel,\n"
" skipValidation: true\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"By using the `secondaryButtons` configuration, we’ve added a Cancel button to the left side of the form. On mobile devices, secondary buttons show up below the primary buttons. We've also configured the Cancel button to display a confirmation message in case the user clicks it by accident. Finally, we styled the Submit button as `\"PRIMARY\"`.\n\n"
"To see that the form goes away even when there are validation errors, we will test this recipe in process.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: a (Text), b (Text), cancel (Boolean)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!a`, `local!b`, `local!cancel`\n"
"1. In your expression, replace:\n"
" - `local!a` with `ri!a`\n"
" - `local!b` with `ri!b`\n"
" - `local!cancel` with `ri!cancel`\n"
"1. In your process model, create variables: aaa (Text) with no value, bbb (Text) with value `\"abc@\"`, cancel (Boolean) with value `false`\n"
"1. On the **Process Start Form** tab in **Process Model Properties**, check the \"Use an existing form\" checkbox, click on \"SAIL Form\", and configure the SAIL Form as `=rule!sailRecipe(a: pv!aaa, b: pv!bbb, cancel: pv!cancel)`\n"
"1. Save and publish the process model.\n"
"1. Start a new process.\n\n"
"### Test it out ###\n\n"
"1. On the start form, click the \"Submit\" button without entering or modifying any of the field values. Notice that the form doesn't submit due to the presence of the validation messages.\n"
" - When testing offline, the form queues for submission but returns the validation messages when you go back online and the form attempts to submit.\n"
"1. Click the \"Cancel\" button without entering or modifying any of the field values. Notice that the form submits despite the invalid fields.\n"
"1. Enter a value in the required field and remove the \"@\" character from the second field. Now click the \"Submit\" button.\n\n"
"---\n"
"layout: basic\n"
"title: Display a Placeholder Text in a Dropdown \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Display a placeholder text as the default option in a dropdown. If the dropdown is marked as required, the user must select a different option before proceeding.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" /* null is not a valid value when there's no placeholder */\n"
" local!a: 10,\n"
" local!b,\n"
" local!c,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Dropdown with Placeholder Text\",\n"
" firstColumnContents: {\n"
" a!dropdownField(\n"
" label: \"No Placeholder\",\n"
" choiceLabels: {\"Fruits\", \"Vegetables\"},\n"
" choiceValues: {10, 20},\n"
" value: local!a,\n"
" saveInto: local!a,\n"
" required: true\n"
" ),\n"
" a!dropdownField(\n"
" label: \"With Placeholder (Optional)\",\n"
" choiceLabels: {\"Fruits\", \"Vegetables\"},\n"
" placeholderLabel: \"--- Select One ---\",\n"
" choiceValues: {10, 20},\n"
" value: local!b,\n"
" saveInto: local!b\n"
" ),\n"
" a!dropdownField(\n"
" label: \"With Placeholder (Required)\",\n"
" choiceLabels: {\"Fruits\", \"Vegetables\"},\n"
" placeholderLabel: \"--- Select One ---\",\n"
" choiceValues: {10, 20},\n"
" value: local!c,\n"
" saveInto: local!c,\n"
" required: true\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Click \"Submit\" without changing the selected values. Notice that the validation message only appears under the last dropdown field.\n"
" - When testing offline, the validation message only appears after you go back online and the form attempts to submit.\n"
"1. Select a value in the last dropdown field, and then click \"Submit\". Notice that there are no validation messages.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: a (Number Integer), b (Number Integer), c (Number Integer)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!a`, `local!b`, `local!c`\n"
"1. In your expression, replace:\n"
" - `local!a` with `ri!a`\n"
" - `local!b` with `ri!b`\n"
" - `local!c` with `ri!c`\n"
"1. In your process model, create variables: foodTypeA (Number Integer) with value `10`, foodTypeB (Number Integer) with with value `=null`, foodTypeC (Number Integer) with with value `=null`\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(a: ac!foodTypeA, b: ac!foodTypeB, c: ac!foodTypeC)`\n"
" - On a start form: `=rule!sailRecipe(a: pv!foodTypeA, b: pv!foodTypeB, c: pv!foodTypeC)`\n\n"
"### Notable implementation details ###\n\n"
"- Process variables of type Number (Integer) default to 0 rather than null, so we have to explicitly set them to null instead of leaving the value blank.\n\n"
"---\n"
"layout: basic\n"
"title: Configure a Dropdown Field to Save a CDT \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" When using a dropdown to select values from the database, or generally from an array of CDT values, configure it to save the entire CDT value rather than just a single field.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!foodTypes: {\n"
" {id: 1, name: \"Fruits\"},\n"
" {id: 2, name: \"Vegetables\"}\n"
" },\n"
" local!selectedFoodType,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Dropdown with CDT\",\n"
" firstColumnContents: {\n"
" a!dropdownField(\n"
" label: \"Food Type\",\n"
" instructions: \"Value saved: \" & local!selectedFoodType,\n"
" choiceLabels: index(local!foodTypes, \"name\", null),\n"
" placeholderLabel: \"--- Select Food Type ---\",\n"
" /* choiceValues gets the CDT/dictionary rather than the ids */\n"
" choiceValues: local!foodTypes,\n"
" value: local!selectedFoodType,\n"
" saveInto: local!selectedFoodType\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Select the choices in the dropdown and notice that the instructions are updated to reflect the value of the variable that is saved, in this case the entire CDT.\n"
" - When testing offline, the instructions do not update, but the same value is saved.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface input: cdt (Any Type)\n"
"1. Delete local variable: `local!selectedFoodType`\n"
"1. In your expression, replace:\n"
" - `local!selectedFoodType` with `ri!cdt`\n"
" - The dictionary array (value of `local!foodTypes`) with a CDT array\n"
"1. In your process model, create a variable called cdt that is of the same type as the CDT array with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(cdt: ac!cdt)`\n"
" - On a start form: `=rule!sailRecipe(cdt: pv!cdt)`\n\n"
"### Notable implementation details ###\n\n"
"- Saving the entire CDT saves you from having to store the id and query the entire object separately when you need to display attributes of the selected CDT elsewhere on the form.\n"
"- When you configure your dropdown, replace the value of `local!foodTypes` with a CDT array that is the result of `a!queryEntity()` or `queryrecord()`. These functions allow you to retrieve only the fields that you need to configure your dropdown.\n"
" - See also: [SAIL Design](SAIL_Design.md)\n"
"- This technique is well suited for selecting lookup values for nested CDTs. Let's say you have a project CDT and each project can have zero, one, or many team members. Team members reference the employee CDT. Use this technique when displaying a form to the end user for selecting team members.\n\n"
"---\n"
"layout: basic\n"
"title: Configure a Dropdown with an Extra Option for Other \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Show a dropdown that has an \"Other\" option at the end of the list of choices. If the user selects \"Other\", show a required text field.\n\n"
"We describe two approaches: the first populates a dropdown list with parallel arrays of data while the second populates the list with an array of CDT values. Each approach has an additional expression that is suited for offline use.\n\n"
"**Expression 1:** This expression shows a dropdown whose option labels and values come from parallel arrays.\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!choiceLabels: {\"Fruits\", \"Vegetables\", \"Other\"},\n"
" local!choiceValues: {10, 20, -1},\n"
" local!selectedFoodType: tointeger(null),\n"
" local!other,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Dropdown from parallel arrays with Other option\",\n"
" firstColumnContents: {\n"
" a!dropdownField(\n"
" label: \"Food Type\",\n"
" instructions: \"Value saved: \" & local!selectedFoodType,\n"
" choiceLabels: local!choiceLabels,\n"
" placeholderLabel: \"--- Select Food Type ---\",\n"
" choiceValues: local!choiceValues,\n"
" value: local!selectedFoodType,\n"
" saveInto: local!selectedFoodType\n"
" ),\n"
" if(\n"
" local!selectedFoodType = -1,\n"
" a!textField(\n"
" label: \"Other\",\n"
" value: local!other,\n"
" saveInto: local!other,\n"
" required: true\n"
" ),\n"
" {}\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\",\n"
" value: null,\n"
" saveInto: if(\n"
" or(isnull(local!selectedFoodType), local!selectedFoodType = -1),\n"
" /* Clear value if user selected Other `*/\n"
" local!selectedFoodType,\n"
" /*` Clear value if user selected an available option */\n"
" local!other\n"
" )\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"**Expression 1 (Offline):** This expression shows how to modify the above expression for offline use.\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!choiceLabels: {\"Fruits\", \"Vegetables\", \"Other\"},\n"
" local!choiceValues: {10, 20, -1},\n"
" local!selectedFoodType: tointeger(null),\n"
" local!other,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Dropdown from parallel arrays with Other option\",\n"
" firstColumnContents: {\n"
" a!dropdownField(\n"
" label: \"Food Type\",\n"
" instructions: \"Value saved: \" & local!selectedFoodType,\n"
" choiceLabels: local!choiceLabels,\n"
" placeholderLabel: \"--- Select Food Type ---\",\n"
" choiceValues: local!choiceValues,\n"
" value: local!selectedFoodType,\n"
" saveInto: local!selectedFoodType\n"
" ),\n"
" a!textField(\n"
" label: \"Other\",\n"
" instructions: \"Required if Food Type is Other\",\n"
" value: local!other,\n"
" saveInto: local!other,\n"
" required: local!selectedFoodType = -1\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\",\n"
" value: null,\n"
" saveInto: if(\n"
" or(isnull(local!selectedFoodType), local!selectedFoodType = -1),\n"
" /* Clear value if user selected Other `*/\n"
" local!selectedFoodType,\n"
" /*` Clear value if user selected an available option */\n"
" local!other\n"
" )\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"**Expression 2:** This expression shows a dropdown whose options come from a CDT array, to which we append an extra entry for the \"Other\" option.\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!foodTypes: {\n"
" {id: 1, name: \"Fruits\"},\n"
" {id: 2, name: \"Vegetables\"}\n"
" },\n"
" local!choices: append(local!foodTypes, {id: -1, name: \"Other\"}),\n"
" local!selectedFoodType,\n"
" local!other,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Dropdown from CDT array with Other option\",\n"
" firstColumnContents: {\n"
" a!dropdownField(\n"
" label: \"Food Type\",\n"
" instructions: \"Value saved: \" & local!selectedFoodType,\n"
" choiceLabels: index(local!choices, \"name\", {}),\n"
" placeholderLabel: \"--- Select Food Type ---\",\n"
" choiceValues: local!choices,\n"
" value: local!selectedFoodType,\n"
" saveInto: local!selectedFoodType\n"
" ),\n"
" if(\n"
" and(\n"
" not(isnull(local!selectedFoodType)),\n"
" tointeger(local!selectedFoodType.id) = -1\n"
" ),\n"
" a!textField(\n"
" label: \"Other\",\n"
" value: local!other,\n"
" saveInto: local!other,\n"
" required: true\n"
" ),\n"
" {}\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\",\n"
" value: null,\n"
" saveInto: if(\n"
" or(\n"
" isnull(local!selectedFoodType), \n"
" tointeger(local!selectedFoodType.id) = -1\n"
" ),\n"
" /* Clear value if user selected Other `*/\n"
" local!selectedFoodType,\n"
" /*` Clear value if user selected an available option */\n"
" local!other\n"
" )\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"**Expression 2 (Offline):** This expression shows how to modify the above expression for offline use.\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!foodTypes: {\n"
" {id: 1, name: \"Fruits\"},\n"
" {id: 2, name: \"Vegetables\"}\n"
" },\n"
" local!choices: append(local!foodTypes, {id: -1, name: \"Other\"}),\n"
" local!selectedFoodType,\n"
" local!other,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Dropdown from CDT array with Other option\",\n"
" firstColumnContents: {\n"
" a!dropdownField(\n"
" label: \"Food Type\",\n"
" instructions: \"Value saved: \" & local!selectedFoodType,\n"
" choiceLabels: index(local!choices, \"name\", {}),\n"
" placeholderLabel: \"--- Select Food Type ---\",\n"
" choiceValues: local!choices,\n"
" value: local!selectedFoodType,\n"
" saveInto: local!selectedFoodType\n"
" ),\n"
" a!textField(\n"
" label: \"Other\",\n"
" value: local!other,\n"
" saveInto: local!other,\n"
" required: and(\n"
" not(isnull(local!selectedFoodType)),\n"
" tointeger(local!selectedFoodType.id) = -1\n"
" )\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\",\n"
" value: null,\n"
" saveInto: if(\n"
" or(\n"
" isnull(local!selectedFoodType), \n"
" tointeger(local!selectedFoodType.id) = -1\n"
" ),\n"
" /* Clear value if user selected Other `*/\n"
" local!selectedFoodType,\n"
" /*` Clear value if user selected an available option */\n"
" local!other\n"
" )\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Select \"Other\" in the dropdown, enter a value and click on the Submit button.\n"
"1. Select \"Fruits\" in the dropdown and click on the Submit button.\n"
"1. Select \"Other\" in the dropdown, don't enter any value, and click on the Submit button. Notice that the form can't be submitted unless the user enters a value in the text field.\n"
"1. When the text field is blank, select \"Fruits\" from the dropdown to hide the text field. You can successfully submit even though the local!other variable is null.\n\n"
"### To write your data to process ###\n\n"
"*Expression 1*\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: selectedFoodType (Number Integer), other (Text)\n"
"1. Delete local variables: `local!selectedFoodType`, `local!other`\n"
"1. In your expression, replace:\n"
" - `local!selectedFoodType` with `ri!selectedFoodType`\n"
" - `local!other` with `ri!other`\n"
"1. In your process model, create variables: selectedFoodType (Number Integer) with no value, other (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(selectedFoodType: ac!selectedFoodType, other: ac!other)`\n"
" - On a start form: `=rule!sailRecipe(selectedFoodType: pv!selectedFoodType, other: pv!other)`\n\n"
"*Expression 2*\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create your CDT with at least 2 fields, one for the selected id, and one for the label to show in the dropdown.\n"
"1. Create interface inputs: cdt (Any Type), other (Text)\n"
"1. Delete local variables: `local!selectedFoodType`, `local!other`\n"
"1. In your expression, replace:\n"
" - `local!selectedFoodType` with `ri!cdt`\n"
" - `tointeger(local!selectedFoodType.id)` with `ri!cdt.id`\n"
" - `local!other` with `ri!other`\n"
" - The value of `local!foodTypes` with a CDT array\n"
"1. If the id field in your CDT is not an integer, also replace `-1` with a value of the same type as your id field.\n"
"1. In your process model, create a variable called `cdt` that is of the same type as the CDT array and a variable called `other` of type Text\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(cdt: ac!cdt, other: ac!other)`\n"
" - On a start form: `=rule!sailRecipe(cdt: pv!cdt, other: pv!other)`\n\n"
"### Notable implementation details ###\n\n"
"- Notice that we cleared out the opposite variable upon submission so that only one variable gets updated. That is, if the user filled out the \"Other\" field and then switched the dropdown back to an available option, `local!other` would be set to null on submission of the form.\n"
"- When you configure your dropdown, replace the value of local!foodTypes with `a!queryEntity()` or `queryrecord()` to return your array of options.\n\n"
"---\n"
"layout: basic\n"
"title: Configure Cascading Dropdowns \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Show different dropdown options depending on the user selection.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" /* Initialize local!make to a null integer because local variables have no type\n"
" * and the comparison local!make=1 would fail.\n"
" * When saving into a process variable or a node input, you don't need to initialize\n"
" * it in this way because the variable will have the correct type. */\n"
" local!make: tointeger(null),\n"
" local!model,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Cascading Dropdowns\",\n"
" firstColumnContents: {\n"
" a!dropdownField(\n"
" label: \"Make\",\n"
" choiceLabels: {\"Subaru\", \"Toyota\"},\n"
" placeholderLabel: \"--- Select Make ---\",\n"
" choiceValues: {1, 2},\n"
" value: local!make,\n"
" saveInto: {\n"
" local!make,\n"
" a!save(local!model, null)\n"
" }\n"
" ),\n"
" with(\n"
" local!cascadingChoices: if(\n"
" local!make=1,\n"
" {{id: 1, label: \"Forester\"}, {id: 2, label: \"Legacy\"}},\n"
" {{id: 10, label: \"Camry\" }, {id: 20, label: \"Yaris\" }}\n"
" ),\n"
" a!dropdownField(\n"
" label: \"Model\",\n"
" choiceLabels: local!cascadingChoices.label,\n"
" placeholderLabel: \"--- Select Model ---\",\n"
" choiceValues: local!cascadingChoices.id,\n"
" value: local!model,\n"
" saveInto: local!model,\n"
" disabled: isnull(local!make)\n"
" )\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Select \"Subaru\" in the first dropdown. Notice that the second dropdown is now enabled and shows the Subaru models. Select a model.\n"
"1. Next, change the first dropdown to \"Toyota\" and notice that the second dropdown now shows the placeholder label so that the user can select a model applicable to \"Toyota\".\n\n"
"### Notable implementation details ###\n\n"
"The value of the second dropdown is reset to null when the first dropdown's value changes to ensure that the value of the selected model always matches what the user sees in the UI.\n\n"
"### Offline ###\n\n"
"Since components cannot be added dynamically when offline, you should include all of the dropdown fields initially in case they are needed. To support this use case for offline, we will create a different expression with a supporting rule.\n\n"
"Create expression rule `ucCascadingDropdownEach` with the following rule inputs:\n\n"
"- makeChoices (Any Type)\n"
"- make (Text)\n"
"- model (Text)\n"
"- index (Integer)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!modelChoices: if(\n"
" ri!index=1,\n"
" {{id: 1, label: \"Forester\"}, {id: 2, label: \"Legacy\"}},\n"
" {{id: 10, label: \"Camry\"}, {id: 20, label: \"Yaris\"}}\n"
" ),\n"
" local!selectedModel,\n"
" a!dropdownField(\n"
" label: ri!makeChoices[ri!index].label & \" Model\",\n"
" choiceLabels: local!modelChoices.label,\n"
" placeholderLabel: \"--- Select \" & ri!makeChoices[ri!index].label & \" Model ---\",\n"
" choiceValues: local!modelChoices.id,\n"
" value: local!selectedModel,\n"
" saveInto: {\n"
" local!selectedModel, \n"
" a!save(ri!model, if(ri!make=ri!makeChoices[ri!index].id, save!value, null))\n"
" },\n"
" validations: if(\n"
" or(ri!make=ri!makeChoices[ri!index].id, isnull(local!selectedModel)), \n"
" null, \n"
" \"This field must be left blank because it does not correspond to your selected Make\"\n"
" ),\n"
" required: ri!make=ri!makeChoices[ri!index].id\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"Now create your main interface with the following definition:\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!makeChoices: {{id: 1, label: \"Subaru\"}, {id: 2, label: \"Toyota\"}},\n"
" local!make,\n"
" local!model,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Cascading Dropdowns (Offline)\",\n"
" firstColumnContents: {\n"
" a!dropdownField(\n"
" label: \"Make\",\n"
" placeholderLabel: \"--- Select Make ---\",\n"
" choiceLabels: local!makeChoices.label,\n"
" choiceValues: {1, 2},\n"
" value: local!make,\n"
" saveInto: local!make\n"
" ),\n"
" a!applyComponents(\n"
" function: rule!ucCascadingDropdownEach(makeChoices: local!makeChoices, make: local!make, model: local!model, index: _),\n"
" array: 1+enumerate(count(local!makeChoices))\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. There are now 3 dropdowns available to the user immediately.\n"
" - If the user selects a make but does not select a model for that make, he will encounter a validation message when the form submits when he is back online.\n"
" - Similarly, if the user selects a model for a make not corresponding to his selected make, he will encounter a validation message when the form submits when he is back online.\n\n"
"### Notable implementation details ###\n"
"A validation message was used when the user selects a model that does not correspond to the selected make to get the user to change the invalid data in the case of a mistake. To simply discard the invalid data on submission, you can remove this validation message.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: make (Number Integer), model (Number Integer)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!make`, `local!model`\n"
"1. In your expression, replace:\n"
" - `local!make` with `ri!make`\n"
" - `local!model` with `ri!model`\n"
"1. In your process model, create variables: make (Number Integer), model (Number Integer)\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters with value `=null`\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(make: ac!make, model: ac!model)`\n"
" - On a start form: `=rule!sailRecipe(make: pv!make, model: pv!model)`\n\n"
"---\n"
"layout: basic\n"
"title: Configure a Boolean Checkbox \n"
"category: sailrecipe\n"
"---\n"
"### Goal ###\n"
" Configure a checkbox that saves a boolean (true/false) value, and validate that the user selects the checkbox before submitting a form.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!userAgreed,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Required Boolean Checkbox\",\n"
" firstColumnContents: {\n"
" a!checkboxField(\n"
" label: \"Acknowledge\",\n"
" choiceLabels: {\"I agree to the terms and conditions.\"},\n"
" choiceValues: {true},\n"
" value: local!userAgreed,\n"
" saveInto: local!userAgreed,\n"
" required: true,\n"
" requiredMessage: \"You must check this box!\"\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label:\"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Click \"Submit\" without selecting the checkbox. Notice that the custom message shows up. Appian recommends using a custom required message when you have a single required checkbox.\n"
"1. Select the checkbox and click \"Submit\".\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface input: userAgreed (Boolean)\n"
"1. Remove the `load()` function\n"
"1. Delete local variable: `local!userAgreed`\n"
"1. In your expression, replace:\n"
" - `local!userAgreed` with `ri!userAgreed`\n"
"1. In your process model, create variable: userAgreed (Boolean) with no value\n"
" - On a task form, create node input\n"
" - On a start form, create process parameter\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(userAgreed: ac!userAgreed)`\n"
" - On a start form: `=rule!sailRecipe(userAgreed: pv!userAgreed)`\n\n"
"---\n"
"layout: basic\n"
"title: Make a Component Required Based on a User Selection \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Make a paragraph component conditionally required based on the user selection.\n\n"
"*Note:* This design pattern is not recommended for offline interfaces because the conditional requiredness of a field based on user selection requires a connection to the server.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!isMinor,\n"
" local!comments,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Conditionally Required Field\",\n"
" firstColumnContents: {\n"
" a!checkboxField(\n"
" label: \"Is Minor\",\n"
" instructions: \"Value saved: \" & local!isMinor,\n"
" choiceLabels: \"Check the box if the patient is a minor\",\n"
" choiceValues: {true},\n"
" value: local!isMinor,\n"
" saveInto: local!isMinor\n"
" ),\n"
" a!paragraphField(\n"
" label: \"Comments\",\n"
" value: local!comments,\n"
" saveInto: local!comments,\n"
" required: local!isMinor\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Select the *Is Minor* checkbox. Notice that the *Comments* field is required. If the checkbox is selected but no comments are entered, the user cannot submit the form.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: isMinor (Boolean), comments (Text)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!isMinor`, `local!comments`\n"
"1. In your expression, replace:\n"
" - `local!isMinor` with `ri!isMinor`\n"
" - `local!comments` with `ri!comments`\n"
"1. In your process model, create variables: isMinor (Boolean) with no value, comments (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(isMinor: ac!isMinor, comments: ac!comments)`\n"
" - On a start form: `=rule!sailRecipe(isMinor: pv!isMinor, comments: pv!comments)`\n\n"
"---\n"
"layout: basic\n"
"title: Set the Default Value Based on a User Input \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Set the default value of a variable based on what the user enters in another component.\n\n"
"*Note:*\n\n"
" - This design pattern is not recommended for offline interfaces because the conditional setting of a value based on user input requires a connection to the server.\n"
" - This example only applies when the default value is based on the user's input in another component. See [Set the Default Value of an Input on a Task Form](#Set_the_Default_Value_of_an_Input_on_a_Task_Form) recipe when the default value must be set as soon as the form is displayed and without requiring the user to interact with the form.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!username,\n"
" local!email,\n"
" local!emailModified: false,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Default Value Based on User Input\",\n"
" firstColumnContents: {\n"
" a!textField(\n"
" label: \"Username\",\n"
" instructions: \"Value saved: \" & local!username,\n"
" value: local!username,\n"
" saveInto: {\n"
" local!username,\n"
" if(local!emailModified, {}, a!save(local!email, append(save!value, \"@example.com\")))\n"
" },\n"
" refreshAfter: \"KEYPRESS\"\n"
" ),\n"
" a!textField(\n"
" label: \"Email\",\n"
" instructions: \"Value saved: \" & local!email,\n"
" value: local!email,\n"
" saveInto: {\n"
" local!email,\n"
" a!save(local!emailModified, true)\n"
" }\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Type into the *Username* field and notice that the *Email* field is pre-populated.\n"
"1. Type into the *Username* field, then modify the *Email* value, and type into the *Username* field again. The *Email* field is no longer pre-populated.\n\n"
"Notice that the value of username as well as the email address field are updated as you type. That's because the username input is configured with `refreshAfter: \"KEYPRESS\"`\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: username (Text), email (Text)\n"
"1. Delete local variables: `local!username`, `local!email`\n"
"1. In your expression, replace:\n"
" - `local!username` with `ri!username`\n"
" - `local!email` with `ri!email`\n"
"1. In your process model, create variables: username (Text) with no value, email (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(username: ac!username, email: ac!email)`\n"
" - On a start form: `=rule!sailRecipe(username: pv!username, email: pv!email)`\n\n"
"---\n"
"layout: basic\n"
"title: Set the Default Value of CDT Fields Based on a User Input \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Set the value of a CDT field based on a user input.\n\n"
"*Note:* This design pattern is not recommended for offline interfaces because the conditional setting of a value based on user input requires a connection to the server.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!myCdt: type!LabelValue(),\n"
" a!formLayout(\n"
" label: \"SAIL Example: Default Value Based on User Input\",\n"
" instructions: \"local!myCdt: \" & local!myCdt,\n"
" firstColumnContents: {\n"
" a!textField(\n"
" label: \"Label\",\n"
" instructions: \"Value saved: \" & local!myCdt.label,\n"
" value: local!myCdt.label,\n"
" saveInto: {\n"
" local!myCdt.label,\n"
" a!save(local!myCdt.value, append(save!value, \"@example.com\"))\n"
" },\n"
" refreshAfter: \"KEYPRESS\"\n"
" ),\n"
" a!textField(\n"
" label: \"Value\",\n"
" instructions: \"Value saved: \" & local!myCdt.value,\n"
" value: local!myCdt.value,\n"
" saveInto: local!myCdt.value,\n"
" refreshAfter: \"KEYPRESS\"\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Type into the first text field, and notice that the second text field is pre-populated. The instructions of the form show the value of the CDT variable.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface input: myCdt (Any Type)\n"
"1. Remove the `load()` function\n"
"1. Delete local variable: `local!myCdt`\n"
"1. In your expression, replace:\n"
" - `local!myCdt` with `ri!myCdt`\n"
"1. In your process model, create variables: myCdt (LabelValue) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(myCdt: ac!myCdt)`\n"
" - On a start form: `=rule!sailRecipe(myCdt: pv!myCdt)`\n\n"
"---\n"
"layout: basic\n"
"title: Format the User's Input \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Format the user's input as a telephone number in the US and save the formatted value, not the user's input.\n\n"
"*Note:* This design pattern is not recommended for offline interfaces because the conditional formatting of a user input requires a connection to the server.\n\n"
"This expression uses the `text()` function to format the telephone number. You may choose to format using your own rule, so you would create the supporting rule first, and then create an interface with the main expression.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!telephone,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Format US Telephone Number\",\n"
" firstColumnContents: {\n"
" a!textField(\n"
" label: \"Telephone\",\n"
" instructions: \"Value saved: \" & local!telephone,\n"
" value: local!telephone,\n"
" saveInto: a!save(local!telephone, text(save!value, \"###-###-####;###-###-####\"))\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Enter `1234567890` then click somewhere else on the form. Notice that the phone number is now formatted.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface input: telephone (Text)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!telephone`\n"
"1. In your expression, replace:\n"
" - `local!telephone` with `ri!telephone`\n"
"1. In your process model, create variables: telephone (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(telephone: ac!telephone)`\n"
" - On a start form: `=rule!sailRecipe(telephone: ac!telephone)`\n\n"
"---\n"
"layout: basic\n"
"title: Gather Sensitive Data from a User and Encrypt It</a> \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Create a form that gathers sensitive information from a user and store it securely in an Encrypted Text variable.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!firstName,\n"
" local!lastName,\n"
" local!ssn,\n"
" local!ailment,\n"
" a!formLayout(\n"
" label: \"New Patient Visit\",\n"
" firstColumnContents: {\n"
" a!textField(\n"
" label: \"First Name\",\n"
" value: local!firstName,\n"
" saveInto: local!firstName,\n"
" required: true\n"
" ),\n"
" a!textField(\n"
" label: \"Last Name\",\n"
" value: local!lastName,\n"
" saveInto: local!lastName,\n"
" required: true\n"
" ),\n"
" a!encryptedTextField(\n"
" label: \"Social Security Number\",\n"
" instructions: \"xxx-xx-xxxx\",\n"
" value: local!ssn,\n"
" saveInto: local!ssn,\n"
" required: true\n"
" ),\n"
" a!encryptedTextField(\n"
" label: \"Primary Ailment\",\n"
" value: local!ailment,\n"
" saveInto: local!ailment\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Enter values for the fields on the start form, and click Submit to see that there are no errors.\n"
"1. The values entered in the EncryptedTextField components are encrypted before being stored. To see the encrypted values, test this form in process.\n\n"
"### Note ###\n\n"
"1. Values generated from an encryptedTextField component will cause an error if you try to display them in a textField component, but can be displayed in clear text using an encryptedTextField component, as in this example.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: firstName (Text), lastName (Text), ssn (Encrypted Text), and ailment (Encrypted Text)\n"
"1. Delete local variables: `local!firstName`, `local!lastName`, `local!ssn`, and `local!ailment`\n"
"1. In your expression, replace:\n"
" - `local!firstName` with `ri!firstName`\n"
" - `local!lastName` with `ri!lastName`\n"
" - `local!ssn` with `ri!ssn`\n"
" - `local!ailment` with `ri!ailment`\n"
"1. In your process model, create variables: firstName (Text), lastName (Text), ssn (Encrypted Text), and ailment (Encrypted Text), all with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(firstName: ac!firstName, lastName: ac!lastName, ssn: ac!ssn, ailment: ac!ailment)`\n"
" - On a start form: `=rule!sailRecipe(firstName: pv!firstName, lastName: pv!lastName, ssn: pv!ssn, ailment: pv!ailment)`\n"
"1. If you interrogate your process variables after starting a process, entering values, and submitting the form, you will see that the variable contains an encrypted value.\n\n"
"---\n"
"layout: basic\n"
"title: Add Custom Validation Rules \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Enforce that the user enters no more than a certain number of characters in their text field, e.g. to match the size constraint on a database column.\n\n"
"\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!a,\n"
" local!b,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Custom Validation Rules\",\n"
" firstColumnContents:{\n"
" a!textField(\n"
" label: \"5 Characters\",\n"
" instructions: \"Character count: \" & len(local!a) & \"/5\",\n"
" value: local!a,\n"
" saveInto: local!a,\n"
" refreshAfter: \"KEYPRESS\",\n"
" validations: if(len(local!a)<=5, null, \"Enter no more than 5 characters\")\n"
" ),\n"
" a!textField(\n"
" label: \"Text\",\n"
" value: local!b,\n"
" saveInto: local!b,\n"
" validations: \"Any text you enter is invalid\"\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Type more than 5 characters in the first text field to see the validation message. The form cannot be submitted while the validation message is displayed.\n"
" - Notice that the second text component doesn't show the validation message until the user types into it. This is because the text value isn't considered invalid until it has a value. See also: [Validating User Inputs](SAIL_Design.md#Validating_User_Inputs)\n"
" - When testing offline, the form queues for submission but returns the validation messages when you go back online and the form attempts to submit.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: a (Text), b (Text)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!a`, `local!b`\n"
"1. In your expression, replace:\n"
" - `local!a` with `ri!a`\n"
" - `local!b` with `ri!b`\n"
"1. In your process model, create variables: aaa (Text) with no value, bbb (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(a: ac!aaa, b: ac!bbb)`\n"
" - On a start form: `=rule!sailRecipe(a: pv!aaa, b: pv!bbb)`\n\n"
"---\n"
"layout: basic\n"
"title: Add Multiple Validation Rules to One Component \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Enforce that the user enters at least a certain number of characters in their text field, and also enforce that it contains the \"@\" character.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!a,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Multiple Validation Rules on One Component\",\n"
" firstColumnContents:{\n"
" a!textField(\n"
" label: \"Text\",\n"
" instructions: \"Enter at least 5 characters, and include the @ character\",\n"
" value: local!a,\n"
" saveInto: local!a,\n"
" validations: {\n"
" if(len(local!a)>5, null, \"Enter at least 5 characters\"),\n"
" if(isnull(local!a), null, if(find(\"@\", local!a)<>0, null, \"You need an @ character!\"))\n"
" }\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Type fewer than 5 characters and click \"Submit\".\n"
" - When testing offline, the form queues for submission but returns the validation messages when you go back online and the form attempts to submit.\n"
"1. Type more than 5 characters but no \"@\" and click \"Submit\".\n"
" - When testing offline, the form queues for submission but returns the validation messages when you go back online and the form attempts to submit.\n"
"1. Type more than 5 characters and include \"@\" and click \"Submit\".\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface input: a (Text)\n"
"1. Remove the `load()` function\n"
"1. Delete local variable: `local!a`\n"
"1. In your expression, replace:\n"
" - `local!a` with `ri!a`\n"
"1. In your process model, create variable: aaa (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(a: ac!aaa)`\n"
" - On a start form: `=rule!sailRecipe(a: pv!aaa)`\n\n"
"---\n"
"layout: basic\n"
"title: Define a Simple Currency Component \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Show a text field that allows the user to enter dollar amounts including the dollar symbol and thousand separators, but save the value as a decimal rather than text. Additionally, always show the dollar amount with the dollar symbol.\n\n"
"*Note:* This design pattern is not recommended for offline interfaces because immediate feedback for and formatting of a user input requires a connection to the server.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!amount,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Simple Currency Component\",\n"
" firstColumnContents:{\n"
" a!textField(\n"
" label:\"Amount in Text\",\n"
" instructions: \"Type of local!amount: \" & typename(typeof(local!amount)),\n"
" value: if(isnull(local!amount), \"\", dollar(local!amount)),\n"
" saveInto: a!save(local!amount, todecimal(save!value))\n"
" ),\n"
" if(\n"
" isnull(local!amount),\n"
" {},\n"
" a!textField(\n"
" label: \"Divided by 10\",\n"
" value: local!amount/10,\n"
" readOnly: true\n"
" )\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Enter `$12345` and click away from the field. Notice that the text box shows $12,345.00 and that the saved value is a decimal.\n"
"1. Enter `$12,345.23` and click away from the field.\n"
"1. Enter `a1b2c3` and click away. Notice that the text box removes the non-numeric characters and treats the remaining as a decimal value. A true currency component would catch this as an error case, hence why this is called a simple currency example.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface input: amount (Number Decimal)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!amount`\n"
"1. In your expression, replace:\n"
" - `local!amount` with `ri!amount`\n"
"1. In your process model, create variables: amount (Number Decimal) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(amount: ac!amount)`\n"
" - On a start form: `=rule!sailRecipe(amount: pv!amount)`\n\n"
"*Note:* If you want to save this example as a reusable component, see also: [Creating Reusable Custom Components](SAIL_Design.md#Creating_Reusable_Custom_Components).\n\n"
"---\n"
"layout: basic\n"
"title: Add Multiple Text Components Dynamically \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Show a dynamic number of text components to simulate a multi-text input box. A new text box is shown as soon as the user starts typing into the last input box.\n\n"
"The main expression uses two supporting rules, so let's create them first.\n\n"
"- `ucDynamicFieldsUpdateArray`: Updates the guest array at the specified index, and appends a null item at the end of the array. If the last item in the array is already null, no new item is added.\n"
"- `ucDynamicFieldEach`: Returns a text field populated with the guest value at the given index.\n\n"
"Create expression rule `ucDynamicFieldsUpdateArray` with the following rule inputs:\n\n"
"- index (Number Integer)\n"
"- guests (Text Array)\n"
"- newValue (Text)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=with(\n"
" local!newGuestList: updatearray(ri!guests, ri!index, ri!newValue),\n"
" if(\n"
" isnull(local!newGuestList[count(local!newGuestList)]),\n"
" local!newGuestList,\n"
" append(local!newGuestList, \"\")\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"Create expression rule `ucDynamicFieldEach` with the following rule inputs:\n\n"
"- index (Number Integer)\n"
"- guests (Text Array)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=a!textField(\n"
" label: if(ri!index=1, \"Guest Names\", \"\"),\n"
" value: ri!guests[ri!index],\n"
" saveInto: a!save(ri!guests, rule!ucDynamicFieldsUpdateArray(ri!index, ri!guests, save!value)),\n"
" refreshAfter: \"KEYPRESS\"\n"
")\n"
"{% endhighlight %}\n\n"
"Now that we've created the two supporting rules, let's move on to the main expression.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!guests: {\"\"},\n"
" a!formLayout(\n"
" label: \"SAIL Example: Add Text Components Dynamically\",\n"
" firstColumnContents: {\n"
" /* The guests array is passed to the rule directly, creating a partial function */\n"
" a!applyComponents(\n"
" function: rule!ucDynamicFieldEach(index: _, guests: local!guests),\n"
" array: 1+enumerate(count(local!guests))\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Type into the text field and notice that an empty one is appended.\n\n"
"**Offline** \n\n"
"Since components cannot be added dynamically when offline, you should include multiple text fields initially in case they are needed. To support this use case for offline, we will create a different expression with a different supporting rule.\n\n"
"Create expression rule `ucTextFieldEach` with the following rule inputs:\n\n"
"- index (Number Integer)\n"
"- guests (Text Array)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=a!textField(\n"
" label: if(ri!index=1, \"Guest Names\", \"\"),\n"
" labelPosition: if(ri!index=1, \"ABOVE\", \"COLLAPSED\"),\n"
" value: ri!guests[ri!index],\n"
" saveInto: ri!guests[ri!index]\n"
")\n"
"{% endhighlight %}\n\n"
"Now create your main interface with the following definition:\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!guests: repeat(5, \"\"),\n"
" /* Replace 5 with the maximum number of text fields that you expect will be needed */\n"
" a!formLayout(\n"
" label: \"SAIL Example: Add Text Components Dynamically\",\n"
" firstColumnContents: {\n"
" /* The guests array is passed to the rule directly, creating a partial function */\n"
" a!applyComponents(\n"
" function: rule!ucTextFieldEach(guests: local!guests, index: _),\n"
" array: 1+enumerate(count(local!guests))\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\",\n"
" saveInto: a!save(local!guests, reject(fn!isnull, local!guests))\n"
" /* This will remove any null values from the array upon submission */\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. There are now 5 text fields available to the user immediately.\n"
"1. Enter values in some of the text fields but leave others blank and submit the form. Notice that null values are removed from the array and only non-null values are saved.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface input: guests (Text Array)\n"
"1. Remove the `load()` function\n"
"1. Delete local variable: `local!guests`\n"
"1. In your expression, replace:\n"
" - `local!guests` with `ri!guests`\n"
"1. In your process model, create variables: guests (Text Array) with value `\"\"`\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(guests: ac!guests)`\n"
" - On a start form: `=rule!sailRecipe(guests: pv!guests)`\n\n"
"---\n"
"layout: basic\n"
"title: Add Multiple File Upload Components Dynamically \n"
"category: sailrecipe\n"
"---\n\n"
"**Goal**: Show a dynamic number of file upload components. After a file is uploaded, add a new file upload component for another file to be uploaded should it be necessary. If an uploaded file is removed, remove its component so the set of components always has exactly one empty spot.\n\n"
"The main expression uses two supporting rules, so let's create them first.\n\n"
"- `ucMultiFileUploadResizeArray`: Updates an array with a value at an index, growing the array if the index is past the end and shrinking the array if the value is null.\n"
"- `ucMultiFileUploadRenderField`: Returns a file upload component populated with the value at the given index.\n\n"
"Create expression rule `ucMultiFileUploadResizeArray` with the following rule inputs:\n\n"
"- array (Document Array)\n"
"- index (Integer)\n"
"- value (Document)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=if(\n"
" /* Do we need to add? `*/\n"
" and(\n"
" /*` Yes, if the value isn't null `*/\n"
" not(isnull(ri!value)),\n"
" /*` and we're at the end of the list `*/ \n"
" ri!index = 1 + length(ri!array) \n"
" ),\n"
" append(\n"
" ri!array,\n"
" /*` Append the new value to the end of the list `*/\n"
" ri!value\n"
" ),\n"
" if(\n"
" /*` Do we need to remove? `*/\n"
" /*` Only if value is now null (value was removed) `*/\n"
" isnull(ri!value),\n"
" /*` Remove: remove the list item for this field `*/\n"
" remove(ri!array, ri!index),\n"
" /*` Neither add nor remove, so insert at index */\n"
" insert(\n"
" ri!array,\n"
" ri!value,\n"
" ri!index\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"Create expression rule `ucMultiFileUploadRenderField` with the following rule inputs:\n\n"
"- label (Text)\n"
"- files (Document Array)\n"
"- target (Document or Folder)\n"
"- index (Integer)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=with(\n"
" local!paddedArray: append(ri!files, null),\n"
" a!fileUploadField(\n"
" label: if(ri!index = 1, ri!label, \"\"),\n"
" target: ri!target,\n"
" value: local!paddedArray[ri!index],\n"
" saveInto: {\n"
" a!save(ri!files, ucMultiFileUploadResizeArray(ri!files, ri!index, save!value))\n"
" }\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"Now that we've created the two supporting rules, let's move on to the main expression.\n\n"
"**Expression**:\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!files: {},\n"
" /* Update the target to a real folder to actually persist the files! */\n"
" local!target: tofolder(-1),\n"
" a!formLayout(\n"
" label: \"SAIL Example: Multiple File Upload\",\n"
" firstColumnContents: {\n"
" a!applyComponents(\n"
" function: rule!ucMultiFileUploadRenderField(label: \"Upload Files\", files: local!files, target: local!target, index: _),\n"
" array: 1 + fn!enumerate(1 + length(local!files))\n"
" ),\n"
" a!textField(\n"
" label: \"local!files value\",\n"
" value: local!files,\n"
" readOnly: true\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"**Test it out**:\n\n"
"1. Upload a file and notice that an empty one is appended.\n"
"1. Upload several more files, then try removing one from the beginning or middle of the list. Notice the empty component is removed.\n\n"
"**Offline** \n\n"
"Since components cannot be added dynamically when offline, you should include multiple file upload fields initially in case they are needed. To support this use case for offline, we will create a different expression with a different supporting rule.\n\n"
"Create expression rule `ucFileUploadEach` with the following rule inputs:\n\n"
"- files (Document Array)\n"
"- target (Document or Folder)\n"
"- index (Number Integer)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=a!fileUploadField(\n"
" label: if(ri!index=1, \"Upload Files\", \"\"),\n"
" labelPosition: if(ri!index=1, \"ABOVE\", \"COLLAPSED\"),\n"
" target: ri!target,\n"
" value: ri!files[ri!index],\n"
" saveInto: ri!files[ri!index]\n"
")\n"
"{% endhighlight %}\n\n"
"Now create your main interface with the following definition:\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!files: repeat(5,null),\n"
" /* Replace 5 with the maximum number of files that you expect will need to be uploaded */\n"
" /* Update the target to a real folder to actually persist the files! */\n"
" local!target: tofolder(-1),\n"
" a!formLayout(\n"
" label: \"SAIL Example: Multiple File Upload (Offline)\",\n"
" firstColumnContents: {\n"
" a!applyComponents(\n"
" function: rule!ucFileUploadEach(files: local!files, target: local!target, index: _),\n"
" array: 1+enumerate(count(local!files))\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\",\n"
" saveInto: a!save(local!files, reject(fn!isnull, local!files))\n"
" /* This will remove any null values from the array upon submission */\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. There are now 5 file upload fields available to the user immediately.\n"
"1. Upload files to some of the fields but leave others blank and submit the form. Notice that null values are removed from the array and only non-null values are saved.\n\n"
"**To write your data to process**:\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create or choose a destination folder for the uploaded files\n"
"1. Create interface input: files (Document Array), target (Document or Folder)\n"
"1. Remove the `load()` function\n"
"1. Delete local variable: `local!files`, `local!target`\n"
"1. In your expression, replace:\n"
" - `local!files` with `ri!files`\n"
" - `tofolder(-1)` with `ri!target`\n"
"1. In your process model, create variables: files (Document Array) with no value, target (Document or Folder) with the value of a system folder.\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(files: ac!files, target: ac!target)`\n"
" - On a start form: `=rule!sailRecipe(files: pv!files, target: pv!target)`\n\n"
"### Notable implementation details ###\n\n"
"- The process variable `pv!target` can be used in one of two ways. You can set it to a constant of type Document or Folder that points to a system folder, if you want to hardcode the folder in your process. If you're also creating the folder in process, you can pass that folder into `pv!target` as well.\n\n"
"---\n"
"layout: basic\n"
"title: Add and Populate Sections Dynamically \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Add and populate a dynamic number of sections, one for each item in a CDT array. Each section contains an input for each field of the CDT. A new entry is added to the CDT array as the user is editing the last section to allow the user to quickly add new entries without extra clicks. Sections can be independently removed by clicking on a \"Remove\" button. In the example below, attempting to remove the last section simply blanks out the inputs. Your own use case may involve removing the last section altogether.\n\n"
"The main expression uses a few supporting rules, so let's create them first.\n\n"
"- `ucDynamicSectionAddOne`: Takes an array of records, and adds a null record of the same type if the given index is for the last item in the array.\n"
"- `ucDynamicSectionRemoveFromArray`: Removes the item at the given index from the records array. If removing the last item in the array, replaces the last item with a null record of the same type.\n"
"- `ucDynamicSectionEach`: Returns a section with its components populated with the value of the record at the specified index.\n\n"
"Create expression rule `ucDynamicSectionAddOne` with the following rule inputs:\n\n"
"- array (Any Type)\n"
"- index (Number Integer)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=if(\n"
" ri!index <> count(ri!array),\n"
" ri!array,\n"
" append(ri!array, cast(typeof(ri!array), null))\n"
")\n"
"{% endhighlight %}\n\n"
"Create expression rule `ucDynamicSectionRemoveFromArray` with the following rule inputs:\n\n"
"- index (Number Integer)\n"
"- array (Any Type)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=if(\n"
" count(ri!array)=1,\n"
" {cast(typeof(ri!array), null)},\n"
" remove(ri!array, ri!index)\n"
")\n"
"{% endhighlight %}\n\n"
"Create interface `ucDynamicSectionEach` with the following interface inputs:\n\n"
"- index (Number Integer)\n"
"- records (Any Type)\n"
"- recordTokens (Any Type)\n\n"
"Enter the following definition for the interface:\n\n"
"{% highlight sail %}\n\n"
"=a!sectionLayout(\n"
" label: \"Section \" & ri!index,\n"
" firstColumnContents:{\n"
" a!textField(\n"
" label: \"Label\",\n"
" value: ri!records[ri!index].label,\n"
" saveInto: {\n"
" ri!records[ri!index].label,\n"
" a!save(ri!records, rule!ucDynamicSectionAddOne(ri!records, ri!index)),\n"
" a!save(ri!recordTokens, rule!ucDynamicSectionAddOne(ri!recordTokens, ri!index))\n"
" },\n"
" refreshAfter: \"KEYPRESS\"\n"
" ),\n"
" a!textField(\n"
" label: \"Value\",\n"
" value: ri!records[ri!index].value,\n"
" saveInto: ri!records[ri!index].value,\n"
" refreshAfter: \"KEYPRESS\"\n"
" ),\n"
" if(\n"
" count(ri!records) > 1,\n"
" a!buttonArrayLayout(\n"
" a!buttonWidget(\n"
" label: \"Remove\",\n"
" value: ri!index,\n"
" saveInto: {\n"
" a!save(ri!records, rule!ucDynamicSectionRemoveFromArray(save!value, ri!records)),\n"
" a!save(ri!recordTokens, rule!ucDynamicSectionRemoveFromArray(save!value, ri!recordTokens))\n"
" }\n"
" )\n"
" ),\n"
" {}\n"
" )\n"
" }\n"
")\n"
"{% endhighlight %}\n\n"
"Now that we've created the supporting rules, let's move on to the main expression.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!records: {type!LabelValue()},\n"
" local!recordTokens,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Add Sections Dynamically\",\n"
" instructions: \"local!records: \" & local!records,\n"
" firstColumnContents: {\n"
" a!applyComponents(\n"
" function: rule!ucDynamicSectionEach(index: _, records: local!records, recordTokens: local!recordTokens),\n"
" array: 1+enumerate(count(local!records)),\n"
" arrayVariable: local!recordTokens\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Fill in the first field and notice that a new section is added as you're typing.\n"
"1. Add a few sections and click on the Remove button to remove items from the array.\n\n"
"**Offline** \n\n"
"Since sections cannot be added dynamically when offline, you should include multiple sections initially in case they are needed. To support this use case for offline, we will create a different expression with a different supporting rule.\n\n"
"Create expression rule `ucSectionEach` with the following rule inputs:\n\n"
"- index (Number Integer)\n"
"- records (Any Type)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=a!sectionLayout(\n"
" label: \"Section \" & ri!index,\n"
" firstColumnContents: {\n"
" a!textField(\n"
" label: \"Label\",\n"
" value: ri!records[ri!index].label,\n"
" saveInto: ri!records[ri!index].label\n"
" ),\n"
" a!textField(\n"
" label: \"Value\",\n"
" value: ri!records[ri!index].value,\n"
" saveInto: ri!records[ri!index].value\n"
" )\n"
" }\n"
")\n"
"{% endhighlight %}\n\n"
"Now create your main interface with the following definition:\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!records: repeat(3, type!LabelValue()),\n"
" /* Replace 3 with the maximum number of sections that you expect will be needed */\n"
" a!formLayout(\n"
" label: \"SAIL Example: Multiple Sections (Offline)\",\n"
" firstColumnContents: {\n"
" a!applyComponents(\n"
" function: rule!ucSectionEach(index: _, records: local!records),\n"
" array: 1+enumerate(count(local!records))\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\",\n"
" saveInto: a!save(local!records, reject(fn!isnull, local!records))\n"
" /* This will remove any null values from the array upon submission */\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. There are now 5 sections available to the user immediately.\n"
"1. Fill out some of the sections but leave others blank and submit the form. Notice that null values are removed from the array and only non-null values are saved.\n\n"
"### Notable implementation details ###\n\n"
"- `local!recordTokens` must be declared as a `load()` local variable as shown above for adding and removing of sections to work correctly. The local variable is then passed to the looping function `a!applyComponents` as its third parameter. a!applyComponents will create an array in this variable that is the same length as the record array. Changes to the record array such as adding, removing, or swapping must also be made to the recordTokens array.\n"
"- When dynamically adding and generating SAIL components in this way, always use the `a!xxxComponents()` looping functions. See also: [New Looping Functions for Components](System_Functions.md#a!applyComponents.28.29)\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface input: records (Any Type)\n"
"1. Delete local variable: `local!records`\n"
"1. In your expression, replace:\n"
" - `local!records` with `ri!records`\n"
"1. In your process model, create variables: records (LabelValue array) as an array with a single null value, such as `{type!LabelValue()}`\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(records: ac!records)`\n"
" - On a start form: `=rule!sailRecipe(records: pv!records)`\n\n"
"---\n"
"layout: basic\n"
"title: Display an Array of Images Stored in Document Management \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Display a set of images stored in Appian's document management system.\n\n"
"*Note:* This design pattern is not recommended for offline interfaces because images do not render when offline.\n\n"
"First, upload a few images into Appian. Then, create a constant of type Document and supporting rule.\n\n"
"- `UC_IMAGE_DOCS`: An array of images in Document Management.\n"
"- `ucDocumentImageEach`: Returns a DocumentImage for use by the image field component. The document name is used as the caption.\n\n"
"Create constant `UC_IMAGE_DOCS` of type Document and select Multiple. Select the images that you uploaded as the value.\n\n"
"Create expression rule `ucDocumentImageEach` with the following rule input:\n\n"
"- document (Document)\n\n"
"Enter the following definition for the rule:\n\n"
"{% highlight sail %}\n\n"
"=a!documentImage(\n"
" document: ri!document,\n"
" caption: document(ri!document, \"name\")\n"
")\n"
"{% endhighlight %}\n\n"
"Now that we've created the supporting constant and rule, let's move on to the main expression.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=a!imageField(\n"
" label: \"Images\",\n"
" size: \"THUMBNAIL\",\n"
" images: apply(rule!ucDocumentImageEach, cons!UC_IMAGE_DOCS)\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n"
"1. Click on an image and navigate the array using the mouse or cursor keys.\n\n\n"
"---\n"
"layout: basic\n"
"title: Display Images in a Grid \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Display a set of images in a read-only paging grid. The images are stored in Appian's document management system.\n\n"
"*Note:* This design pattern is not recommended for offline interfaces because images do not render when offline.\n\n"
"First, upload a few images into Appian. Then, create a constant of type document named `UC_IMAGE_DOCS` and select the Multiple checkbox. Select the images that you uploaded as the value.\n\n"
"Now that we've created the supporting constant, let's move on to the main expression.\n\n"
"{% highlight sail %}\n\n"
"=a!gridField(\n"
" label: \"SAIL Example: Grid with Images\",\n"
" totalCount: count(cons!UC_IMAGE_DOCS),\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Document Name\",\n"
" data: apply(fn!document, cons!UC_IMAGE_DOCS, \"name\")\n"
" ),\n"
" a!gridImageColumn(\n"
" label: \"Image Thumbnail Column\",\n"
" data: apply(a!documentImage(document: _), cons!UC_IMAGE_DOCS),\n"
" size: \"THUMBNAIL\"\n"
" )\n"
" },\n"
" value: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: count(cons!UC_IMAGE_DOCS)\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Notable implementation details ###\n\n"
"* Because we did not configure any paging or sorting, clicking on the column headers will not sort the columns.\n\n"
" * See also: [Filter the Data in a Grid](#Filter_the_Data_in_a_Grid)\n\n"
"* If your images are appropriate to show at a 20 x 20px size you can use `size: \"ICON\"` instead of `size: \"THUMBNAIL\"`.\n\n"
"---\n"
"layout: basic\n"
"title: Add a Custom Required Message \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Instead of the product message that shows up when a required field has no value, show a custom message.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!a,\n"
" local!b,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Custom Required Message\",\n"
" firstColumnContents:{\n"
" a!textField(\n"
" label: \"Custom Message\",\n"
" value: local!a,\n"
" saveInto: local!a,\n"
" required: true,\n"
" requiredMessage: \"You must enter a value!\"\n"
" ),\n"
" a!textField(\n"
" label: \"Product Message\",\n"
" value: local!b,\n"
" saveInto: local!b,\n"
" required: true\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Leave both text fields blank and click \"Submit\".\n"
" - The custom required message appears regardless of whether you are online or offline.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: a (Text), b (Text)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!a`, `local!b`\n"
"1. In your expression, replace:\n"
" - `local!a` with `ri!a`\n"
" - `local!b` with `ri!b`\n"
"1. In your process model, create variables: aaa (Text) with no value, bbb (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(a: ac!aaa, b: ac!bbb)`\n"
" - On a start form: `=rule!sailRecipe(a: pv!aaa, b: pv!bbb)`\n\n"
"---\n"
"layout: basic\n"
"title: Showing Validation Errors that Aren't Specific to One Component \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Alert the user about form problems that aren't specific to one component, showing the message only when the user clicks \"Submit\". In this case, there are two fields and although neither are required, at least one of them must be filled out to submit the form.\n\n"
"\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!phone,\n"
" local!email,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Showing Form Errors on Submission\",\n"
" firstColumnContents:{\n"
" a!textField(\n"
" label: \"Phone Number\",\n"
" value: local!phone,\n"
" saveInto: local!phone\n"
" ),\n"
" a!textField(\n"
" label: \"Email Address\",\n"
" value: local!email,\n"
" saveInto: local!email\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: a!buttonWidgetSubmit(\n"
" label: \"Submit\"\n"
" )\n"
" ),\n"
" validations: {\n"
" if(\n"
" and(isnull(local!phone), isnull(local!email)),\n"
" a!validationMessage(\n"
" message: \"You must enter either a phone number or an email address!\",\n"
" validateAfter: \"SUBMIT\"\n"
" ),\n"
" {}\n"
" )\n"
" }\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Leave both text fields blank and click \"Submit\".\n"
" - When testing offline, the form queues for submission but returns the validation message when you go back online and the form attempts to submit.\n\n"
"### Notable implementation details ###\n\n"
"- The system function a!validationMessage() allows us to specify whether the validation message is shown right away (`REFRESH`) or when the user submits the form (`SUBMIT`). If the validation message should always be shown right away, we could just pass the message to `a!formLayout()`'s `validations` parameter as Text. To show multiple messages, we can pass a list of Text, a list of `a!validationMessage()`, or a mix of the two.\n"
"- You can also configure `a!sectionLayout()` to show validation messages:\n\n"
"\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: phone (Text), email (Text)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!phone`, `local!email`\n"
"1. In your expression, replace:\n"
" - `local!phone` with `ri!phone`\n"
" - `local!email` with `ri!email`\n"
"1. In your process model, create variables: phone (Text) with no value, email (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(phone: ac!phone, email: ac!email)`\n"
" - On a start form: `=rule!sailRecipe(phone: pv!phone, email: pv!email)`\n\n"
"---\n"
"layout: basic\n"
"title: Approve/Reject Buttons with Conditional Requiredness \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Present two buttons to the end user called \"Approve\" and \"Reject\" and only make the comments field required if the user clicks \"Reject\".\n\n"
"*Note:* \n\n"
" - This design pattern is not recommended for offline interfaces because the conditional requiredness of a field based on user interaction requires a connection to the server.\n"
" - `validationGroup` can have any string that you define. See also: [Using Validation Groups](SAIL_Design.md#Adding_Validation_to_Button_Components_using_Validation_Groups)\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!comments,\n"
" local!hasApproved,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Approve Reject Buttons with Conditional Requiredness\",\n"
" firstColumnContents: {\n"
" a!paragraphField(\n"
" label: \"Comments\",\n"
" instructions: \"This also shows an example of a custom required message\",\n"
" value: local!comments,\n"
" saveInto: local!comments,\n"
" required: true,\n"
" requiredMessage: \"You must enter comments when you reject\",\n"
" validationGroup: \"reject\"\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: {\n"
" a!buttonWidgetSubmit(\n"
" label: \"Approve\",\n"
" value: true,\n"
" saveInto: local!hasApproved\n"
" ),\n"
" a!buttonWidgetSubmit(\n"
" label: \"Reject\",\n"
" value: false,\n"
" saveInto: local!hasApproved,\n"
" validationGroup: \"reject\"\n"
" )\n"
" }\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Click \"Reject\" without entering any comments. Notice that the custom required message that we configured using the `requiredMessage` parameter shows up rather than the generic product message.\n"
"1. Click Approve without entering any comments.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: comments (Text), hasApproved (Boolean)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!comments`, `local!hasApproved`\n"
"1. In your expression, replace:\n"
" - `local!comments` with `ri!comments`\n"
" - `local!hasApproved` with `ri!hasApproved`\n"
"1. In your process model, create variables: comments (Text) with no value, hasApproved (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(comments: ac!comments, hasApproved: ac!hasApproved)`\n"
" - On a start form: `=rule!sailRecipe(comments: ac!comments, hasApproved: ac!hasApproved)`\n\n"
"---\n"
"layout: basic\n"
"title: Approve/Reject Buttons with Multiple Validation Rules \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Present two buttons to the end user called \"Approve\" and \"Reject\". Also display a comments field. The number of characters in the comments field must not exceed 100 characters, regardless of the button clicked. Additionally, the user must enter comments if she clicks \"Reject\" (comments are required in this case).\n\n"
"*Note:* \n\n"
" - This design pattern is not recommended for offline interfaces because the conditional requiredness of a field based on user interaction requires a connection to the server.\n"
" - We recommend that you go through the [Approve/Reject Buttons with Conditional Requiredness](SAIL_Recipes.md#Approve/Reject_Buttons_with_Conditional_Requiredness) recipe before working on this one.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!comment,\n"
" local!hasApproved,\n"
" a!formLayout(\n"
" label: \"SAIL Example: Approve Reject Buttons with Multiple Validation Rules\",\n"
" firstColumnContents: {\n"
" with(\n"
" local!commentIsValid: if(len(local!comment)<=100, true, false),\n"
" a!paragraphField(\n"
" label: \"Comments\",\n"
" instructions: \"Comments must have no more than 100 characters, regardless of the button clicked. Comments are required if rejecting.\",\n"
" value: local!comment,\n"
" saveInto: local!comment,\n"
" required: local!commentIsValid,\n"
" requiredMessage: \"You must enter comments when you reject\",\n"
" validations: if(local!commentIsValid, \"\", \"Comments must have no more than 100 characters\"),\n"
" validationGroup: if(local!commentIsValid, \"reject\", \"\")\n"
" )\n"
" )\n"
" },\n"
" buttons: a!buttonLayout(\n"
" primaryButtons: {\n"
" a!buttonWidgetSubmit(\n"
" label: \"Approve\",\n"
" value: true,\n"
" saveInto: local!hasApproved\n"
" ),\n"
" a!buttonWidgetSubmit(\n"
" label: \"Reject\",\n"
" value: false,\n"
" saveInto: local!hasApproved,\n"
" validationGroup: \"reject\"\n"
" )\n"
" }\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Click \"Reject\" without entering any comments. Notice that the custom required message that we configured using the `requiredMessage` parameter shows up rather than the generic product message.\n"
"1. Enter more than 100 characters as comments, and click \"Reject\". Click \"Approve'. You shouldn't be able to submit in either case.\n"
"1. Click \"Approve\" without entering any comments.\n\n"
"### To write your data to process ###\n\n"
"1. Save your interface as sailRecipe\n"
"1. Create interface inputs: comments (Text), hasApproved (Boolean)\n"
"1. Remove the `load()` function\n"
"1. Delete local variables: `local!comments`, `local!hasApproved`\n"
"1. In your expression, replace:\n"
" - `local!comments` with `ri!comments`\n"
" - `local!hasApproved` with `ri!hasApproved`\n"
"1. In your process model, create variables: comments (Text) with no value, hasApproved (Text) with no value\n"
" - On a task form, create node inputs\n"
" - On a start form, create process parameters\n"
"1. In your process model, enter your SAIL Form definition:\n"
" - On a task form: `=rule!sailRecipe(comments: ac!comments, hasApproved: ac!hasApproved)`\n"
" - On a start form: `=rule!sailRecipe(comments: ac!comments, hasApproved: ac!hasApproved)`\n\n"
"---\n"
"layout: basic\n"
"title: Display Data from a Record in a Grid \n"
"category: sailrecipe\n"
"---\n\n"
"**Goal**: Display data from a record type in a read-only paging grid.\n\n"
"\n\n"
"This scenario demonstrates:\n\n"
"- How to use the report builder to generate a grid to display data from a record type.\n"
"- How to modify the generated expression to change the grid column alignment.\n\n"
"For this recipe, you'll need a record. Let's use the process-backed record from the [Records Tutorial](Records_Tutorial.md#Create_Process-Backed_Records). If you haven't already created the Expense Report record type, do so now by completing the first five steps of the \"Create Process-Backed Records\" tutorial and starting at least one instance of the process, then follow the steps below to generate a grid using the report builder.\n\n"
"1. Create a constant called `EXPENSE_REPORT_RECORD` with Record Type as the type and `Expense Report` as the Value.\n"
"1. Open the Interface Designer and select **Report Builder** from the list of templates.\n"
"1. In the *Source Constant* field, select the `EXPENSE_REPORT_RECORD`constant.\n"
"1. In the *Add a field...* dropdown, select `expenseDate` and click **Add Field**.\n"
"1. Set the display name for each of the three columns as `Item`, `Amount`, and `Date`, respectively.\n"
"1. Click **Generate**. You should see the following expression in the design pane on the left-hand side:\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" a!queryColumn(field: \"expenseDate\"),\n"
" }),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: index(local!datasubset.data, \"expenseAmount\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Date\",\n"
" field: \"expenseDate\",\n"
" data: index(local!datasubset.data, \"expenseDate\", null)\n"
" ),\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"<ol start=\"7\">\n"
"<li>Right align the \"Amount\" and \"Date\" columns by modifying the expression as shown below:</li>\n"
"</ol>\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" a!queryColumn(field: \"expenseDate\"),\n"
" }),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: index(local!datasubset.data, \"expenseAmount\", null),\n"
" alignment: \"RIGHT\"\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Date\",\n"
" field: \"expenseDate\",\n"
" data: index(local!datasubset.data, \"expenseDate\", null),\n"
" alignment: \"RIGHT\"\n"
" ),\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Notable implementation details ###\n\n"
"- The grid generated by the report builder is already configured to page and sort.\n"
"- The query that populates this grid will return all data for the record type in batches of 20. To filter the data returned by the query, see also: [Filter Data from a Record in a Grid](#Filter_Data_from_a_Record_in_a_Grid)\n\n"
"---\n"
"layout: basic\n"
"title: Display Data with CDT Fields from a Record in a Grid \n"
"category: sailrecipe\n"
"---\n\n"
"**Goal**: Display data that contains CDT fields from a record type in a read-only paging grid.\n\n"
"\n\n"
"This scenario demonstrates:\n\n"
"- How to use the report builder to generate a grid to display data from a record type.\n"
"- How to modify the generated expression to show data from a nested field in the grid.\n\n"
"For this recipe, you'll need a record. Let's use the process-backed record from the [Records Tutorial](Records_Tutorial.md#Create_Process-Backed_Records). If you haven't already created the Expense Report record type, do so now by completing the first five steps of the \"Create Process-Backed Records\" tutorial and starting at least one instance of the process, then follow the steps below to generate a grid using the report builder.\n\n"
"1. Create a constant called `EXPENSE_REPORT_RECORD` with Record Type as the type and `Expense Report` as the Value.\n"
"1. Open the Interface Designer and select **Report Builder** from the list of templates.\n"
"1. In the *Source Constant* field, select the `EXPENSE_REPORT_RECORD`constant.\n"
"1. Set the display name for each of the columns as `Item` and `Amount`, respectively.\n"
"1. Click **Generate**. You should see the following expression in the design pane on the left-hand side:\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" }),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: index(local!datasubset.data, \"expenseAmount\", null)\n"
" ),\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"<ol start=\"6\">\n"
"<li>Add a new query column for <code>pp.initiator</code> and a corresponding column in the grid by modifying the expression as shown below:</li>\n"
"</ol>\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" a!queryColumn(field: \"pp.initiator\", alias: \"initiator\")\n"
" }),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: index(local!datasubset.data, \"expenseAmount\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Requested By\",\n"
" field: \"initiator\",\n"
" data: index(local!datasubset.data, \"initiator\", null)\n"
" )\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Notable implementation details ###\n\n"
"- The grid generated by the report builder is already configured to page and sort.\n"
"- Nested fields cannot be added through the report builder, so they must be added after the expression is generated.\n"
"- The query that populates this grid will return all data for the record type (in pages). If you want to return only a subset of data, add a default filter to the query. See also: [Filter Data from a Record in a Grid](#Filter_Data_from_a_Record_in_a_Grid)\n\n"
"---\n"
"layout: basic\n"
"title: Format Data from a Record in a Grid \n"
"category: sailrecipe\n"
"---\n\n"
"**Goal**: Format the data from a record type to display in a read-only paging grid, specifically a decimal number as a dollar amount and a username as a user's display name.\n\n"
"\n\n"
"This scenario demonstrates:\n\n"
"- How to use the report builder to generate a grid to display data from a record type.\n"
"- How to modify the generated expression to show data from a nested field in the grid.\n"
"- How to format the data that is returned from the query to display in the grid.\n\n"
"For this recipe, you'll need a record. Let's use the process-backed record from the [Records Tutorial](Records_Tutorial.md#Create_Process-Backed_Records). If you haven't already created the Expense Report record type, do so now by completing the first five steps of the \"Create Process-Backed Records\" tutorial and starting at least one instance of the process, then follow the steps below to generate a grid using the report builder.\n\n"
"1. Create a constant called `EXPENSE_REPORT_RECORD` with Record Type as the type and `Expense Report` as the Value.\n"
"1. Open the Interface Designer and select **Report Builder** from the list of templates.\n"
"1. In the *Source Constant* field, select the `EXPENSE_REPORT_RECORD`constant.\n"
"1. Set the display name for each of the columns as `Item` and `Amount`, respectively.\n"
"1. Click **Generate**. You should see the following expression in the design pane on the left-hand side:\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" }),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: index(local!datasubset.data, \"expenseAmount\", null)\n"
" ),\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"<ol start=\"6\">\n"
"<li>Add a new query column for <code>pp.initiator</code> and a corresponding column in the grid by modifying the expression as shown below:</li>\n"
"</ol>\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" a!queryColumn(field: \"pp.initiator\", alias: \"initiator\")\n"
" }),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: index(local!datasubset.data, \"expenseAmount\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Requested By\",\n"
" field: \"initiator\",\n"
" data: index(local!datasubset.data, \"initiator\", null)\n"
" )\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"<ol start=\"7\">\n"
"<li>Create an expression rule <code>ucUserDisplayName</code> with a single input <code>user</code> of type User and the following definition:</li>\n"
"</ol>\n\n"
"{% highlight sail %}\n\n"
"=user(ri!user, \"firstName\") & \" \" & user(ri!user, \"lastName\"){% endhighlight %}\n\n"
"<ol start=\"8\">\n"
"<li>Format the \"Amount\" column using the <code>dollar</code> function to display the value in dollars and the \"Requested By\" column using <code>rule!ucUserDisplayName</code> to display the user's first and last name by modifying the expression as shown below:</li>\n"
"</ol>\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" a!queryColumn(field: \"pp.initiator\", alias: \"initiator\")\n"
" }),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: dollar(index(local!datasubset.data, \"expenseAmount\", {}))\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Requested By\",\n"
" field: \"initiator\",\n"
" data: apply(rule!ucUserDisplayName, index(local!datasubset.data, \"initiator\", {}))\n"
" )\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Notable implementation details ###\n\n"
"- The grid generated by the report builder is already configured to page and sort.\n"
"- Nested fields cannot be added through the report builder, so they must be added after the expression is generated.\n"
"- The grid displays the text representation of all types, including Appian Objects such as a user, so we applied our own formatting.\n"
"- The query that populates this grid will return all data for the record type (in pages). If you want to return only a subset of data, add a default filter to the query. See also: [Filter Data from a Record in a Grid](#Filter_Data_from_a_Record_in_a_Grid)\n\n"
"---\n"
"layout: basic\n"
"title: Filter Data from a Record in a Grid \n"
"category: sailrecipe\n"
"---\n\n"
"**Goal**: Display data from a record type in a read-only paging grid and a dropdown to allow the user to filter the data that is displayed.\n\n"
"*Note:* This design pattern is not recommended for offline interfaces because filtering data based on user interaction requires a connection to the server.\n\n"
"\n\n"
"This scenario demonstrates:\n\n"
"- How to use the report builder to generate a grid to display data from a record type.\n"
"- How to modify the generated expression to add a filter for the data.\n\n"
"For this recipe, you'll need a record. Let's use the process-backed record from the [Records Tutorial](Records_Tutorial.md#Create_Process-Backed_Records). If you haven't already created the Expense Report record type, do so now by completing the first five steps of the \"Create Process-Backed Records\" tutorial and starting at least one instance of the process, then follow the steps below to generate a grid using the report builder.\n\n"
"1. Create a constant called `EXPENSE_REPORT_RECORD` with Record Type as the type and `Expense Report` as the Value.\n"
"1. Open the Interface Designer and select **Report Builder** from the list of templates.\n"
"1. In the *Source Constant* field, select the `EXPENSE_REPORT_RECORD`constant.\n"
"1. Set the display name for each of the columns as `Item` and `Amount`, respectively.\n"
"1. Click **Generate**. You should see the following expression in the design pane on the left-hand side:\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" }),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: index(local!datasubset.data, \"expenseAmount\", null)\n"
" ),\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"<ol start=\"6\">\n"
"<li>Add a filter to the query and a dropdown so that the user can select what data set to display by modifying the expression as shown below:</li>\n"
"</ol>\n\n"
"{% highlight sail %}\n\n"
"load(\n"
" local!pagingInfo: a!pagingInfo(\n"
" startIndex: 1,\n"
" batchSize: 20,\n"
" sort: a!sortInfo(\n"
" field: \"expenseItem\",\n"
" ascending: true\n"
" )\n"
" ),\n"
" local!priceRange,\n"
" with(\n"
" local!datasubset: queryrecord(\n"
" cons!EXPENSE_REPORT_RECORD,\n"
" a!query(\n"
" selection: a!querySelection(columns: {\n"
" a!queryColumn(field: \"expenseItem\"),\n"
" a!queryColumn(field: \"expenseAmount\"),\n"
" }),\n"
" filter: if(\n"
" isnull(local!priceRange),\n"
" null,\n"
" a!queryFilter(\n"
" field: \"expenseAmount\",\n"
" operator: choose(local!priceRange, \"<\", \"between\", \">\"),\n"
" value: choose(local!priceRange, 100, {100, 200}, 200)\n"
" )\n"
" ),\n"
" pagingInfo: local!pagingInfo\n"
" )\n"
" ),\n"
" {\n"
" a!dropdownField(\n"
" label: \"Filter by Amount\",\n"
" labelPosition: \"ADJACENT\",\n"
" choiceLabels: {\"Less than $100\", \"$100 - $200\", \"Greater than $200\"},\n"
" placeholderLabel: \"All\",\n"
" choiceValues: {1, 2, 3},\n"
" value: local!priceRange,\n"
" saveInto: {\n"
" local!priceRange,\n"
" /* We need to reset the paging info so that it goes back */\n"
" /* to the first page of the grid when the user changes */\n"
" /* the filter. Otherwise, the grid errors out */\n"
" a!save(local!pagingInfo.startIndex, 1)\n"
" }\n"
" ),\n"
" a!gridField(\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"Item\",\n"
" field: \"expenseItem\",\n"
" data: index(local!datasubset.data, \"expenseItem\", null)\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Amount\",\n"
" field: \"expenseAmount\",\n"
" data: index(local!datasubset.data, \"expenseAmount\", null)\n"
" ),\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" }\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Select the \"Less than $100\" option from the filter dropdown. Notice that only items where the amount is less than $100 are displayed in the grid.\n\n"
"### Notable implementation details ###\n\n"
"- The grid generated by the report builder is already configured to page and sort.\n"
"- Notice that when the user makes a selection from the dropdown, we’re always resetting the value of `local!pagingInfo` so that the user always sees the first page of results for the selected filter. This is necessary regardless of what the user has selected, so we ignore the value returned by the component (in this case, the value of the dropdown selection) and instead insert our own value.\n\n"
"---\n"
"layout: basic\n"
"title: Display Array of Data in a Grid \n"
"category: sailrecipe\n"
"---\n\n"
"### Goal ###\n"
" Display an array of CDT data in a read-only paging grid.\n\n"
"\n\n"
"This scenario demonstrates:\n\n"
"- How to display an array of data in a read-only paging grid.\n"
"- How to configure paging and sorting in a read-only grid.\n\n"
"### Expression ###\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!data: {\n"
" {id: 1, name: \"John Smith\", department: \"Engineering\"},\n"
" {id: 2, name: \"Michael Johnson\", department: \"Finance\"},\n"
" {id: 3, name: \"Mary Reed\", department: \"Engineering\"},\n"
" {id: 4, name: \"Angela Cooper\", department: \"Sales\"},\n"
" {id: 5, name: \"Elizabeth Ward\", department: \"Sales\"},\n"
" {id: 6, name: \"Daniel Lewis\", department: \"Human Resources\"}\n"
" },\n"
" /* batchSize is 3 to show more than 1 page of data in this recipe. Increase it as needed. `*/\n"
" local!pagingInfo: a!pagingInfo(startIndex: 1, batchSize: 3, sort: a!sortInfo(field: \"name\", ascending: true)),\n"
" with(\n"
" local!datasubset: todatasubset(local!data, local!pagingInfo),\n"
" a!gridField(\n"
" label: \"SAIL Example: Display Data in a Read-Only Paging Grid\",\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"ID\",\n"
" field: \"id\",\n"
" data: index(local!datasubset.data, \"id\", {}),\n"
" alignment: \"RIGHT\"\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Name\",\n"
" field: \"name\",\n"
" data: index(local!datasubset.data, \"name\", {})\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Department\",\n"
" field: \"department\",\n"
" data: index(local!datasubset.data, \"department\", {})\n"
" )\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Test it out ###\n\n"
"1. Go to the second page of the grid. Notice that grid data changes to show the next batch of data.\n"
"1. Sort the grid by clicking on one of the column headers. Notice that the grid goes back to the first page and displays the data in the correct order.\n\n"
"**Offline** \n\n"
"This expression shows how to modify the above expression for offline use. The only difference is that all rows are displayed initially since grid paging is not available when offline.\n\n"
"{% highlight sail %}\n\n"
"=load(\n"
" local!data: {\n"
" {id: 1, name: \"John Smith\", department: \"Engineering\"},\n"
" {id: 2, name: \"Michael Johnson\", department: \"Finance\"},\n"
" {id: 3, name: \"Mary Reed\", department: \"Engineering\"},\n"
" {id: 4, name: \"Angela Cooper\", department: \"Sales\"},\n"
" {id: 5, name: \"Elizabeth Ward\", department: \"Sales\"},\n"
" {id: 6, name: \"Daniel Lewis\", department: \"Human Resources\"}\n"
" },\n"
" local!pagingInfo: a!pagingInfo(startIndex: 1, batchSize: -1, sort: a!sortInfo(field: \"name\", ascending: true)),\n"
" with(\n"
" local!datasubset: todatasubset(local!data, local!pagingInfo),\n"
" a!gridField(\n"
" label: \"SAIL Example: Display Data in a Read-Only Paging Grid\",\n"
" totalCount: local!datasubset.totalCount,\n"
" columns: {\n"
" a!gridTextColumn(\n"
" label: \"ID\",\n"
" field: \"id\",\n"
" data: index(local!datasubset.data, \"id\", {}),\n"
" alignment: \"RIGHT\"\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Name\",\n"
" field: \"name\",\n"
" data: index(local!datasubset.data, \"name\", {})\n"
" ),\n"
" a!gridTextColumn(\n"
" label: \"Department\",\n"
" field: \"department\",\n"
" data: index(local!datasubset.data, \"department\", {})\n"
" )\n"
" },\n"
" value: local!pagingInfo,\n"
" saveInto: local!pagingInfo\n"
" )\n"
" )\n"
")\n"
"{% endhighlight %}\n\n"
"### Notable implementation details ###\n\n"
"- Notice that `local!pagingInfo` is a `load()` variable and `local!datasubset` is a `with()` variable. This allows us to save a new value into `local!pagingInfo` when the user interacts with the grid and then use that value to re")
matches = regex.finditer(test_str)
for match_num, match in enumerate(matches, start=1):
print(f"Match {match_num} was found at {match.start()}-{match.end()}: {match.group()}")
for group_num, group in enumerate(match.groups(), start=1):
print(f"Group {group_num} found at {match.start(group_num)}-{match.end(group_num)}: {group}")
Please keep in mind that these code samples are automatically generated and are not guaranteed to work. If you find any syntax errors, feel free to submit a bug report. For a full regex reference for Python, please visit: https://docs.python.org/3/library/re.html