Building an Org Chart

This page walks through the steps necessary to create an organization chart for customer contacts.

To assist with this walk-through, a generic Contact Management application can be downloaded. The primary purpose of the application is to instruct you on how to fully leverage the hierarchy browser tree component.

A similar application for the hierarchy browser columns component can be found on the Hierarchy Browser Columns - Portfolio Manager page.

The following sections highlight different pieces of the application and explain how they work. After reading through this detailed explanation of the app, you should have a very good understanding of how the hierarchy browser tree works and how best to leverage its power.

To start, enter an application prefix, for example "CM" as an abbreviation of "Contact Management." Your prefix can be a maximum of 7 letters and numbers. Then, click "Download App".

Application Prefix


After downloading the app, inspect and import the application on your development environment. If you have chosen the same prefix as someone before you, the app will fail the inspection, and you will need to pick a different prefix and re-download the app.

Once the application is on your environment, feel free to skip between sections based on what you want to learn. If you want to learn about the capabilities of the hierarchy browser tree in general, read through the whole page. If all you need to learn is how to build a navigation path from a single value, skip to that section. Before you skip to your section though, read through the application overview below to get a sense of how the application is structured.

Application Overview

The contact management application allows users to view and edit an organizational structure of contacts. The application consists of a single report to view an organizational structure and an action to manage it. The remaining objects all support these two interfaces.

All of your application objects will have a slightly different name than what is described below. This is because of your unique application prefix. The prefix allows everyone on your team to have their own sandbox app to learn with.

The Data

Before going into how the interfaces are made, let's look at the underlying data. The data type has three types of fields to note:

  • Fields like "firstName", "lastName", and "department" store basic personal information about the contact.
  • The "supervisorId" field references another CustomerContact identifier. This field creates the hierarchy to display using a!hierarchyBrowserFieldTree().
  • The "imageId" field references an Appian document that is stored in the Customer Contact Images folder in the Customer Contact KC knowledge center.

All data is stored in a data store, called Customer Contact DS. The data store has a single entity that references the CustomerContact data type. The constant,DSE_CUSTOMER_CONTACT, points to that entity for querying.

The data hierarchy has two main constraints: a contact can only have a single supervisor, and a contact can not be his or her own supervisor. Beyond that, the interface supports multiple contacts with no supervisor (meaning multiple roots to the hierarchy) and explicitly prevents circular relationships (for example, Greg's supervisor cannot be Allison if Allison's supervisor is Bob and Bob's supervisor is Greb). These complexities will give you the most possible experience with the component while preparing you for real-world design challenges.

Before continuing, make sure your Customer Contact DS data store is published and, once published, run the "Create Initial Data" process model. The data store must be published to use the interface and create the dummy data that makes understanding the interfaces below easier.

The Interfaces

The primary pieces of the application are the manageCustomerContacts and contactBrowserDashboard interfaces.

The manageCustomerContacts interface is the top-level interface for the "Manage Customer Contacts" action. It allows you to add, edit, and remove contacts.

The contactBrowserDashboard is the top-level interface for the "Customer Contact Browser" report. It allows you to browse the hierarchy in a read-only state.

It has much of the same content as manageCustomerContacts interface, minus controls to edit the data.

You may be wondering why the application uses an action to manage contact data rather than the report and Smart Services in interfaces. Here are the three primary reasons:

  • Files cannot be uploaded to record views or reports, so we need to use a process to create and update contact images.
  • Only one smart service can be run in the saveInto parameter of a component, but both contact creation and deletion require multiple services.

The Process Model

The "Manage Customer Contacts" process model backs the action to edit contact data. On clicking on the action, the user is taken to the contact management interface. From there, the user can go down 3 main paths:

  • The user can end the action and stop editing contacts.
  • The user can delete a contact. If that contact has subordinates, they are updated.
  • The user can update or create a contact. If that contact is inserted at the root of the hierarchy, all contacts without a supervisor are updated.

After running smart services to delete, update, or create a contact, the process brings the user back to the management form.

Supporting Content

All other objects in the app work to support the interfaces and process model that drive the application. When relevant to the goals of the sections below, these objects will be called out and explained.

Getting the Selected Value

Relevant objects: contactBrowserDashboard, manageCustomerContacts, getSelectedValue

Unlike in the Hierarchy Browser Columns, the hierarchy browser tree's selection is not passed separately to the component. The selected value is, instead, always the last value in the navigation path. In the image above, the navigation path is {"All Contacts", Jane Blake, Chris Smith}, where Jane Blake and Chris Smith are data type values. Because Chris Smith is the final value in the array, he is the selected value and the node is outlined in dark blue.

Throughout the application, the selected value is held in the targetContact variable. In the contactBrowserDashboard interface, targetContact is a local variable. In the manageCustomerContacts interface, targetContact is a rule input. In both interfaces, the variable is set in the hierarchy browser tree's pathSaveInto using the helper rule getSelectedValue. getSelectedValue takes the value at the last index of the navigation path array to get the component's selected value. You can see this in action in the snippet below, taken from contactBrowserDashboard.

1
2
3
4
5
6
7
8
9
10
pathSaveInto: {
  local!path,
  with(
    local!selectedValue: rule!getSelectedValue(local!path),
    a!save(
      {local!targetContact, local!foundContact},
      if(rule!isCustomerContact(local!selectedValue), local!selectedValue, null)
    )
  )
}

As you can see, we first save to local!path to preserve the user's navigation. If local!path is not in the save, the component will not update when the user clicks on a node. Then, value selected is a CustomerContact, we save to local!targetContact—the contact whose details are shown below the browser—and local!foundContact—the contact shown in the contactFinder picker. For explanation of why we need to check to see if the value is a CustomerContact, see the next section on Handling Multiple Roots.

Handling Multiple Roots

Relevant objects: contactBrowserDashboard, isCustomerContact

You may have noticed that the hierarchy browser tree must have a single value in its first level. This value is the "root" of hierarchy. This requirement can be problematic because not all hierarchies have a single root. In the case of customer contacts, it may be that there is no one person that all contacts report to or that the information on the root contact is unavailable or not useful. If that is the case, you are not stuck. There is a solution.

In the Contact Management application, this situation is handled by checking to see if there are multiple roots, and if there are, adding a text value at the start of the navigation path. This generates a single root value where there normally is not one. To see this in the interface configuration, first open up the contactBrowserDashboard.

Notice first that local!path is set using the following expression:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
  If there are no contacts, make local!path null
  If there is only one contact with no supervisor, he or she will be set as the root.
  If there are multiple users with no supervisor, we create a placeholder root.
*/
local!path: if(
  length(local!contactsWithNoSupervisor) = 0,
  null,
  if(
    length(local!contactsWithNoSupervisor) = 1,
    local!contactsWithNoSupervisor[1],
    cons!ROOT_CONTACT_TEXT_READONLY
  )
)

After adding this text value to the path, we need to make sure the hierarchy browser tree can handle that value. This handling of text can be seen in the pathSaveInto, nextLevelValues and nodeConfigs fields of the component. All three leverage the isCustomerContact rule which returns true if the value is of type CustomerContact.

As shown in the section on Getting the Selected Value , the pathSaveInto first checks to see if the selected value is a contact before saving it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pathSaveInto: {
  local!path,
  with(
    local!selectedValue: rule!getSelectedValue(local!path),
    a!save(
      {local!targetContact, local!foundContact},
      /*
        Checks to see if the selected value is a contact before saving it.
        If could be a text-based root node for when multiple contacts have no supervisor
      */
      if(rule!isCustomerContact(local!selectedValue), local!selectedValue, null)
    )
  )
}

Were we to save a text value into the local!foundContact or local!targetContact, it would break the other components that use those values.

The nextLevelValues also first checks to see if the fv!nodeValue is a CustomerContact, then chooses the values to get accordingly.

1
2
3
4
5
6
7
8
9
10
/*
  If the value is a contact, get subordinates.
  If it is not a contact, it is the placeholder root value we created in the path.
    The values in the next level are thus the contacts with no supervisor
*/
nextLevelValues: if(
  rule!isCustomerContact(fv!nodeValue),
  rule!getSubordinatesFromCustomerContact(fv!nodeValue),
  local!contactsWithNoSupervisor
)

Finally, the nodeConfigs input handles the synthesized root value with another check for a CustomerContact v.s. Text value. The check should look very familiar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
  If the value is a contact, use the customer contact node configs.
  If it is not a contact, it is the placeholder root value we created in the path.
    It is a text value, so we use the rule that turns a text value into a node.
*/
nodeConfigs: if(
  rule!isCustomerContact(fv!nodeValue),
  rule!customerContactNodeConfigs(fv!nodeValue),
  rule!textBasedContactBrowserNode(
    text: cons!ROOT_CONTACT_TEXT_READONLY,
    icon: a!iconNewsEvent(icon: "PEOPLE_3"),
    isDrillable: true
  )
)

Building a Navigation Path from a Single Value

Relevant objects: getContactById, buildContactHierarchyPath, buildContactHierarchyPath_helper, contactFinder

One of the main benefits of the hierarchy browser tree is the context it provides. When looking at the contact browser, you can see who reports to a contact and who the contact reports to. You may have noticed, however, that simply providing the contact as the path leaves out that second part; your user will not see who the contact reports to. One option would be to always initialize the component at the root and let the user browse to the contact they want to see. The much better option is to dynamically generate the navigation path that shows the hierarchy from the root to the selected contact without having the user manually browse there. This section shows you how to do that.

The first thing you need in any case where you want to build the navigation path is a rule that, given some value, returns the value above it in the hierarchy. In the contact management app, that means we need a rule that, given a contact, returns the contact's supervisor. Because we have the supervisor's ID in the contact data type, we really just need a rule that can get a contact from its ID. In our app, that rule is called getContactById.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(
  isnull(ri!id),
  null,
  with(
    local!results: a!queryEntity(
      entity: cons!DSE_CUSTOMER_CONTACT,
      query: a!query(
        filter: a!queryFilter("id", "=", ri!id),
        pagingInfo: a!pagingInfo(startIndex: 1, batchSize: 1)
      )
    ),
    cast(
      'type!{urn:com:appian:recipes}CustomerContact',
      if(length(local!results) = 0, null, local!results.data)
    )
  )
)

Next, we need a way to repeatedly call our rule and take the result of the prior evaluation as an input. For calling functions repeatedly, you use Looping Functions. The most commonly used looping function is apply(), but the problem with apply is that each evaluation of the specified rule is ignorant of the other evaluations. Having knowledge of the prior evaluations result is a key facet to the problem. If Sarah reports to Chris, we can easily query for Chris, generate a path of {Chris, Sarah}, and display the tree below.

Things get harder, though, when Chris has a supervisor, and Chris' supervisor has a supervisor, and so on and so forth. We don't know how many people we have to go through until we reach the top, and we don't know the next supervisor to query for until we have the information about a subordinate. What we need is reduce(). With reduce(), we can loop over our query function and return the result to the next evaluation. That way, we can query for Chris, then query again for Chris' supervisor, Jane, then keep going until we find a contact without a supervisor to query for.

The rule that uses reduce() is buildContactHierarchyPath.

1
2
3
4
5
6
7
8
9
10
/* Has a 50 contact limit */
if(
  isnull(ri!contact),
  {},
  reduce(
    rule!buildContactHierarchyPath_helper,
    ri!contact,
    enumerate(50)
  )
)

A few things to point out:

  1. We have to choose ahead of time how many times we run the rule, but this number should be an upper bound. Here, I have arbitrarily chosen 50. We create a limit because reduce needs an array to iterate over, and the array must have a finite number of values. To create the array, we use enumerate().
  2. We need to create a third rule, buildContactHierarchyPath_helper, that calls getContactById reducing over getContactById would only result in the root contact, not the whole path. We need a rule that can keep track of and preserve our list of results that becomes our path.
  3. When iterating over the helper rule, buildContactHierarchyPath_helper, there needs to be a 'dummy' input for the result of enumerate(). It won't be used, but reduce() requires it.

Below is the definition of buildContactHierarchyPath_helper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
with(
  /*
    We use the first value because we are always appending the new value
      to the start of the path so that supervisors come before subordinates
      in the path array.
  */
  local!lastResult: ri!path[1],
  if(
    or(
      /* If there isn't more to query */
      isnull(local!lastResult.supervisorId),
      /* Or, if we've hit a circular reference, stop querying */
      not(exact(union(ri!path.id, tointeger({})), ri!path.id))
    ),
    ri!path,
    /* Otherwise, get the next supervisor to add to the path */
    {
      rule!getContactById(local!lastResult.supervisorId),
      ri!path
    }  
  )
)

First, notice that it is always getting the first value in the array.

1
local!result: ri!path[1]

We use the first value because we are always appending the new value to the start of the path so that supervisors come before subordinates in the path array. This can be seen at the end of the rule where we're putting the result of getContactById ahead of ri!path.

1
2
3
4
{
  rule!getContactById(local!lastResult.supervisorId),
  ri!path
}

Next, notice that we are checking to see if the local!lastResult has a null supervisorId. If it does, we return the ri!path that was passed in because we know we've reached a root.

1
2
3
4
5
6
7
8
9
if(
  or(
    /* If there isn't more to query */
    isnull(local!lastResult.supervisorId),
    ...
  ),
  ri!path,
  ...
)

Last, notice that we make another check in the if() above to ensure that we have only unique values in the array.

1
2
/* Or, if we've hit a circular reference, stop querying */
not(exact(union(ri!path.id, tointeger({})), ri!path.id))

This check is used to account for circular references. In the next section, you'll learn more about this.

To see the path construction in action, open up the contactFinder interface. In the preview search for a contact, select the contact, and watch as ri!path is updated.

Preventing Circular References

Relevant objects: contactInformationSection, buildContactHierarchyPath, buildContactHierarchyPath_helper

In the Contact Management application, a circular reference is when two people, through any number of levels in the hierarchy, can be considered each others' supervisor. In the example below, Jane Blake, Sarah Hernandez, and Chris Smith are in a circular hierarchy. Jane Blake's supervisor is Sarah Hernandez; Sarah Hernandez's supervisor is Chris Smith; and Chris Smith's supervisor is Jane Blake.

In the previous section, we discussed building a navigation path from a single value. The rule we talked about, buildContactHierarchyPath, is the key to preventing circular references.

To see how we do it, open up the contactInformationSection interface. In the contactInformationSection interface, go to the custom picker with the label "Supervisor". This is where contact supervisors can be updated and we run the risk of a user creating a circular reference. To ensure that a user does not create a circular reference, we build the to-be path on setting the prospective supervisor. In building the path, we do not want to repeatedly query for supervisors if the user is trying to create a circular reference. The user experience would suffer. That is why in our buildContactHierarchyPath_helper, we check for duplicate contacts in the path which indicates a circular reference.

If we do not find a circular reference, the user can continue making updates. To improve performance, we can also use the path we created on the supervisor change when the user submits the form and is taken to the new location of the contact.

If we do find a circular reference, we want to trigger a validation that tells the user what is wrong and prevents the form from being submitted. In contactInformationSection, the validation is triggered by a local variable, local!isCircularReference.

1
2
3
4
5
6
7
8
9
10
11
12
/*
  Used for preventing circular references.
  Checks to see whether a new path after a supervisor update
    would result in a hierarchy that contains the target contact
    more than once
*/
local!isCircularReference: length(
  wherecontains(
    index(ri!contact, "id", tointeger(null)),
    tointeger(index(ri!path, "id", {}))
  )
) > 1,

This local variable checks to see if the path contains the target contact more than once. If it does, we know that the user is trying to create a circular reference. So, in the custom picker, we show a validation error.

1
2
3
4
5
6
7
8
9
validations: if(
  ...
  if(
    local!isCircularReference,
    "Two contacts cannot be each other's supervisor",
    ""
  )
  ...
)

With that validation, circular references are prevented.

Adding Values to the Hierarchy

Relevant objects: Manage Customer Contacts, manageCustomerContacts, contactInformationSection, cons!ADD_ROOT_CONTACT_TEXT, cons!ADD_CONTACT_TEXT

As stated in the overview, we cannot use Smart Services in interfaces to make updates. Instead, we need to use a process. The process model responsible for adding, updating, and removing nodes is called Manage Customer Contacts. Looking at the model, you can see that there are two paths: one for adding and updating and one for deleting. Right now, we'll focus on the add case. Within the add case, there are two paths: one for adding a root and one for adding a leaf. More generally, the case for adding a root is really the case for adding any value with a level in the hierarchy below it. The idea is that you need to update both the new contact and all existing subordinates that will report to the new contact.

The interface that populates t the values to update is the manageCustomerContacts interface. There are a few important things to explain in the interface before we dive deeper into the process model.

Open the manageCustomerContacts interface. First, notice that "Add New Contact" nodes are used instead of buttons to allow the user to intuitively add new contacts wherever they want. This style of contact management allows users add three categories of contacts:

  1. Users can add a contact that reports to an existing contact and has no subordinates. This is the simplest case.
  2. Users can add another root to the hierarchy. In the Contact Management example, this means adding a contact without a supervisor and without any subordinates. Doing this successfully requires handling multiple roots.
  3. Users can create a new root that all existing contacts in the hierarchy now report to.

When a user clicks on a node to add a new contact, a few things happen in the save. See the comments in the interface snippet below for a detailed description of what is happening.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
pathSaveInto: if(
  /*
    1. If the user has started editing a new contact or updating an existing contact,
    we block navigation by setting the pathSaveInto to null
  */
  or(local!workStarted, local!actionTaken),
  null,
  /*
    2. If work is NOT in progress, always save the path. We need to save the path
    for the component to respond to a user's click.
  */
  {
    local!path,
    /*
      3. The add new root is dropped from local!path and the result is saved to ri!path.
      ri!path is a CustomerContact list that cannot hold the text value of a new root contact node
    */
    a!save(ri!path, ldrop(local!path, 1)),
    with(
      local!selectedNode: rule!getSelectedValue(local!path),
      /*
        If a contact is clicked
          save the selected node as to the "targetContact" and "foundContact"
        Otherwise, a new contact node is clicked. Therefore:
          Set the new contacts supervisorId to the value above it in the path
      */
      if(
        rule!isCustomerContact(value: local!selectedNode),
        ...,
        /*
          4. `local!newContact.supervisorId` is updated with what will be the new node'ssupervisor.
          We update the `local!newContact.supervisorId` so that the new contact form below
          the hierarchy browser tree shows the correct supervisor
        */
        {
          a!save(
            local!newContact.supervisorId,
            rule!multiIndex(local!path, {length({local!path}) - 1, "id"}, null)
          ),
          /*
            5. Drop add new node from the path.
            ri!path is a CustomerConact list and cannot hold the text value of a new contact node.
          */
          a!save(ri!path, rdrop(local!path, 1)),
          a!save({ri!targetContact, local!foundContact}, null),
          a!save(ri!newOrUpdatedImage, null)
        }
      )
    )
  }
)

Now that the user has clicked a new contact node, the variable local!selectedNode will be a text value (either cons!ADD_ROOT_CONTACT_TEXT or cons!ADD_CONTACT_TEXT). Because local!selectedNode is Text, local!newContactSelected will be true. Since local!newContactSelected is true, the bottons to update or delete a contact disappear.

1
2
3
4
5
6
7
8
9
if(
  or(
    local!newContactSelected, /* Or set to true here */
    local!workStarted,
    local!actionTaken
  ),
  {},
  a!buttonLayout(...)
)

and the contactInformationSection will display along with buttons to create a new contact or cancel the update.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
if(
  local!newContactSelected,
  {
    rule!contactInformationSection(
      contact: local!newContact,
      label: cons!ADD_CONTACT_TEXT,
      newOrUpdatedImage: ri!newOrUpdatedImage,
      path: ri!path
    ),
    a!buttonLayout(
      primaryButtons: {
        a!buttonWidget(
          label: "Create New Contact",
          style: "PRIMARY",
          value: local!newContact,
          saveInto: {
            ri!targetContact,
            a!save(ri!goTo, cons!CUSTOMER_CONTACT_ACTIONS[1]),
            a!save(ri!rootContactAdded, local!selectedNode = cons!ADD_ROOT_CONTACT_TEXT)
          },
          submit: true
        ),
        a!buttonWidget(
          label: "Cancel New Contact",
          disabled: not(local!workStarted),
          value: null,
          saveInto: {
            a!save(local!newContact, 'type!{urn:com:appian:recipes}CustomerContact'()),
            /* Remove the "Add New" from the path when they hit cancel if it's not the root */
            if(local!pathLength > 1, a!save(local!path, rdrop(local!path, 1)), null),
            a!save(ri!path, rdrop({ri!path}, 1)),
            if(
              rule!isCustomerContact(rule!getSelectedValue(local!path)),
              a!save(ri!targetContact, rule!getSelectedValue(local!path)),
              null
            ),
            a!save(
              ri!newOrUpdatedImage,
              index(ri!targetContact, "imageId", null)
            )
          }
        )
      }
    )
  },
  ...
)

Once the user edits any information in the contactInformationSection, the variable local!workStarted will be set to true.

1
2
3
4
5
6
7
8
9
local!workStarted: if(
  local!newContactSelected,
  /*
    Checks to see if the user has added any values. The user does not set the supervisor,
    so our check needs to ignore the supervisorId.
  */
  local!newContact <> 'type!{urn:com:appian:recipes}CustomerContact'(supervisorId: local!newContact.supervisorId),
  ...
)

Remember from earlier that if local!workStarted is true, navigation will be disabled. This means that the user is locked into the new node until they click "Cancel New Contact".

After entering information, when the user clicks "Create New Contact":

  1. local!newContact is saved to ri!targetContact
  2. ri!goTo is updated appropriately to route the proces flow
  3. ri!rootContactAdded is set if the selected node is the root contact text
1
2
3
4
5
6
7
8
9
10
11
a!buttonWidget(
  label: "Create New Contact",
  style: "PRIMARY",
  value: local!newContact,
  saveInto: {
    ri!targetContact,
    a!save(ri!goTo, cons!CUSTOMER_CONTACT_ACTIONS[1]),
    a!save(ri!rootContactAdded, local!selectedNode = cons!ADD_ROOT_CONTACT_TEXT)
  },
  submit: true
)

Once in process, the flow is routed down the update/create flow:

  1. Delete Old Image?
    • If the user has update the image, delete the old one
  2. Update Image & Last Updated
    • pv!targetCotact.lastUpdated is set to now()
    • pv!targetCotact.imageId is updated with the uploaded image if one is present. We need to update this value in process because the document ID is not set until the form is submitted.
  3. Write New Contact or Edit Contact
    • The new contact is written to the DB
  4. Update Path
    • The new contact is appended to the path.
  5. Root Contact Added?
    • If a leaf contact was added (a contact without any subordinates) or a contact was simply updated, route back to the task.
    • Otherwise, a new root was added, and we need to update all contacts without a supervisor to report to the new contact.
      1. Get New Subordinates
        • Get all contacts without a supervisor who will not report to the new root
        • TBD based on UX review
      2. Any Contacts to Update?
        • If there are no new subordinates, route back to the task.
        • Otherwise, continue to update the subordinates
          1. Update Contacts' Supervisors
            • Update new subordinatess supervisorId
          2. Write Updated Contacts
            • Write new subordinates

Deleting Values in the Hierarchy

Relevant objects: manageCustomerContacts, Manage Customer Contacts

When a user clicks on a contact node in the manageCustomerContacts interface, local!newContactSelected is set to false. As a rules, the user has the option to click "Delete Contact". A few things are saved when clicking "Delete Contact".

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a!buttonWidget(
  label: "Delete Contact",
  value: cons!CUSTOMER_CONTACT_ACTIONS[2],
  saveInto: {
    /* 1. `ri!goTo` is updated to take the process flow through the correct path */
    ri!goTo,
    /*
      2. The root node is removed from the path and the deleted value is removed,
      allowing us to bring the user back to the supervisor of the deleted contact.
    */
    a!save(ri!newOrUpdatedImage, null),
    /* 3. The root and target contact are removed from the path */
    a!save(ri!path, rdrop(ldrop(local!path, 1), 1))
  },
  submit: true
)

The Manage Customer Contacts process then handles the deletion of the contact.

  1. Get Subordinates
    • Gets potential subordinates to update. We need to remove their supervisorId
  2. Any Subordinates?
    • If there are no subordinates, route to delete the contact.
    • Otherwise:
    1. Update Subordinates
      • Set the subordinate's supervisorId to null
    2. Write Updated Subordinates
      • Write the updated subordinates
  3. Delete Contact
    • Delete the contact and route back to the contact management form

Updating Values in the Hierarchy

Relevant objects: manageCustomerContacts, contactInformationSection, Manage Customer Contacts

We'll start the explaination in the interface, so open manageCustomerContacts. To update a contact, the user needs to click on a contact node in the management interface. When a user clicks on a contact in the manageCustomerContacts interface, they have the option to click the "Update Contact" button.

On clicking "Update Contact", ri!goTo is updated.

1
2
3
4
5
a!buttonWidget(
  label: "Update Contact",
  value: cons!CUSTOMER_CONTACT_ACTIONS[1],
  saveInto: ri!goTo
)

Once ri!goTo is set, local!actionTaken equals true.

1
local!actionTaken: not(isnull(ri!goTo))

Since local!actionTaken is true and local!newContactSelected is false, the contactInformationSection is populated with the selected contact and becomes editable. In addition, buttons appear below the section that allow the user to cancel or confirm the contact edits.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
if(
  local!newContactSelected,
  ...,
  {
    rule!contactInformationSection(
      contact: ri!targetContact,
      isDisabled: not(local!actionTaken),
      label: "Update " & rule!displayContactFullName(ri!targetContact),
      newOrUpdatedImage: ri!newOrUpdatedImage,
      path: ri!path
    ),
    if(
      not(local!actionTaken),
      {},
      a!buttonLayout(
        primaryButtons: {
          a!buttonWidget(
            label: "Confirm Update",
            style: "PRIMARY",
            submit: true
          ),
          a!buttonWidget(
            label: "Cancel Update",
            value: null,
            saveInto: {
              ri!goTo,
              a!save(ri!newOrUpdatedImage, ri!targetContact.imageId),
              a!save(ri!targetContact, local!selectedNode)
            }
          )
        }
      )
    )
  }
)

The user can freely update the contact's fields. Open up contactInformationSection. In general, when fields are not disabled, user inputs are stored directly into fields in ri!targetContact. The two exceptions are the file upload for the picture of the contact and the custom picker for the contact's supervisor.

1
2
3
4
5
6
7
8
9
10
11
if(
  or(ri!isDisabled, ri!isReadOnly),
  ...,
  a!fileUploadField(
    label: "Image",
    target: cons!CUSTOMER_CONTACT_IMAGE_FOLDER,
    value: ri!newOrUpdatedImage,
    saveInto: ri!newOrUpdatedImage,
    disabled: ri!isDisabled
  )  
)

We need a document type, rather than an integer field in a data type, to hold the image being uploaded because the file upload field requires it. In process, the contact's imageId field will be updated accordingly.

If the supervisor is updated, we need check for circular references and update ri!path appropriately. See the linked section for more information on the check for circular references.

After having made updates, the user can choose to submit the changes by clicking "Confirm Update."

1
2
3
4
5
a!buttonWidget(
  label: "Confirm Update",
  style: "PRIMARY",
  submit: true
)

Once in the Manage Customer Contacts process, the flow follows a similar path to adding a new contact.

  1. Delete Old Image?
    • If the user has update the image, delete the old one (Delete Image)
  2. Update Image & Last Updated
    • pv!targetCotact.lastUpdated is set to now()
    • pv!targetCotact.imageId is updated with the uploaded image if one is present. We need to update this value in process because the document ID is not set until the form is submitted.
  3. Write New Contact or Edit Contact
    • The updated contact is written to the DB
  4. Update Path
    • The selected contact in the path (the final value in the array) is updated to the new, updated contact.
  5. Root Contact Added?
    • A root contact was not added, so it routes the user back to the task.
FEEDBACK