Add, Remove, and Move Group Members Browser

SAIL Recipes give you an opportunity to explore different interface design patterns. To learn how to directly use SAIL recipes within your interfaces, see Adapt a SAIL Recipe to Work with My Applications.

Goal

Display the membership tree for a given group and provide users with the ability to add, remove, and move user members from a single interface.

Manage Group Membership

The user and group browsers provide the ability for users to manage group membership from Tempo. In this recipe, you will learn how to configure an interface that enables users to perform the following operations:

  1. Add user members to a group
  2. Remove user members from a group
  3. Move a user members from one group to another

For this recipe, you will need a group membership tree to test with. We used a department structure within a software engineering company, but feel free to use any hierarchy you want.

Once you have created your groups and users, you will be ready to begin constructing the interface and process by following the steps below:

Expression

  1. From the Application Designer, create a new interface called EX_addMoveRemoveUsers with the following inputs:

    • ri!initialGroup (Group) - the group whose direct members display in the first column of the browser, necessary to re-initialize the component after submittal
    • ri!navigationPath (User or Group array) - the navigation path that the user was seeing when the form was submitted, necessary to re-initialize the component after submittal
    • ri!usersToAdd (User Array) - the users to add as members to the ri!addToGroup
    • ri!addToGroup (Group) - the chosen group to add users to
    • ri!userToRemove (User) - the chosen user for the remove action
    • ri!removeFromGroup (Group) - the group from which the ri!userToRemove is being removed
    • ri!userToMove (User) - the chosen user for the move action
    • ri!moveFromGroup (Group) - the chosen group to move the ri!userToMove from
    • ri!moveToGroup (Group) - the chosen group to move the ri!userToMove to
    • ri!btnAction (Text) - determines flow of the process. Can be "ADD", "REMOVE", or "MOVE"
  2. Copy the following text into the expression view of EX_addMoveRemoveUsers

=load(
  local!selectionValue,
  with(
    local!isUserSelected: runtimetypeof(local!selectionValue) = 'type!{http://www.appian.com/ae/types/2009}User',
    local!actionTaken: if(
      isnull(ri!btnAction),
      false,
      or(ri!btnAction = "ADD", ri!btnAction = "MOVE")
    ),
    local!navigationLength: if(
      isnull(ri!navigationPath),
      0,
      length(ri!navigationPath)
    ),
    a!formLayout(
      label: "Manage Group Membership",
      contents: {
        /*
          If you use this as a related action, rather than using this picker
          to choose the inital group, you would pass it in as context for
          the action.
        */
        a!pickerFieldGroups(
          label: "Select a group to view its members",
          maxSelections: 1,
          value: ri!initialGroup,
          saveInto: {
            ri!initialGroup,
            a!save(
              {
                ri!btnAction,
                ri!userToRemove,
                ri!userToMove,
                ri!addedToGroup,
                ri!removeFromGroup,
                ri!navigationPath,
                ri!moveFromGroup,
                ri!moveToGroup,
                ri!usersToAdd,
                local!selectionValue
              },
              null
            )
          }
        ), 
        a!sectionLayout(
          showWhen: not(isnull(ri!initialGroup)),
          label: group(ri!initialGroup, "groupName") & " Group Members",
          contents: {
            a!buttonArrayLayout(
              buttons: {
                a!buttonWidget(
                  label: "Add Members",
                  disabled: or(
                    local!actionTaken,
                    local!navigationLength = 0
                  ),
                  value: "ADD",
                  saveInto: {
                    ri!btnAction,
                    a!save(
                      ri!addedToGroup,
                      if(
                        /*
                          If the user has not navigated anywhere, or
                          the user has clicked on a user in the first column,
                          save the intial group
                        */
                        or(
                          local!navigationLength = 0,
                          and(local!navigationLength = 1, local!isUserSelected)
                        ),
                        ri!initialGroup,
                        if(
                          /* If a user is selected save the last group in the path */
                          local!isUserSelected,
                          togroup(ri!navigationPath[local!navigationLength - 1])
                          /* Otherwise save the end of the path */
                          togroup(ri!navigationPath[local!navigationLength])
                        )
                      )
                    )
                  }
                ),
                a!buttonWidgetSubmit(
                  label: "Remove Member",
                  disabled: or(local!actionTaken, not(local!isUserSelected)),
                  value: "REMOVE",
                  saveInto: {
                    ri!btnAction,
                    a!save(ri!userToRemove, local!selectionValue),
                    /*
                      Since a user needs to be removed from a group, the button
                      needs to determine what group the user should be removed from.
                    */
                    a!save(
                      ri!removeFromGroup, 
                      if(
                        /* If the user is on the first column, save the initial group */
                        local!navigationLength = 1,
                        ri!initialGroup,
                        /* Otherwise save the group containing the selected user */
                        ri!navigationPath[local!navigationLength - 1]
                      )
                    ),
                    /*
                      This navigation path will be used to pre-populate the group browser 
                      when the user returns to this interface after the selected user has
                      been removed. Therefore, we must remove the selected user from the
                      navigation path to prevent an error.
                    */
                    a!save(ri!navigationPath, rdrop(ri!navigationPath, 1))
                  }
                ),
                
                a!buttonWidget(
                  label: "Move Member",
                  style: "NORMAL",
                  disabled: or(local!actionTaken, not(local!isUserSelected)),
                  value: "MOVE",
                  saveInto: {
                    ri!btnAction,
                    a!save(ri!userToMove, local!selectionValue),
                    /*
                      Since a user needs to be removed from a group, the button
                      needs to determine what group the user should be removed from.
                    */
                    a!save(
                      ri!moveFromGroup, 
                      if(
                        /* If the user is in the first column save the initial group. */
                        local!navigationLength = 1,
                        ri!initialGroup,
                        /*  Otherwise save the last group in the navigation path */
                        ri!navigationPath[local!navigationLength - 1]
                      )
                    ),
                    a!save(ri!navigationPath, rdrop(ri!navigationPath, 1)),
                    a!save(ri!moveToGroup, ri!moveFromGroup)
                  }
                )
                
              }
            ),
            /*
              After selecting a member to move, the interface needs to allow the 
              user to select a group to move the member to. To limit what can
              be selected to a group and not a user, we switch the component
              to a group browser
            */
            a!groupBrowserFieldColumns(
              labelPosition: "COLLAPSED", 
              showWhen: ri!btnAction = "MOVE",
              rootGroup: ri!initialGroup,
              pathValue: ri!navigationPath,
              pathSaveInto: ri!navigationPath,
              selectionValue: ri!moveToGroup,
              selectionSaveInto: ri!moveToGroup
            ),
            /*
              Unless the user is in the process of moving members,
              the user has the option to select a user to move or remove
              or a group to add members to.
            */
            a!userAndGroupBrowserFieldColumns(
              labelPosition: "COLLAPSED",
              showWhen: not(ri!btnAction = "MOVE"),
              rootGroup: ri!initialGroup,
              pathValue: ri!navigationPath,
              pathSaveInto: ri!navigationPath,
              selectionValue: local!selectionValue,
              selectionSaveInto: local!selectionValue,
              readOnly: or(
                ri!btnAction = "ADD",
                ri!btnAction = "REMOVE"
              )
            ),
            /*
              Navigation cannot be cleared without configuration, so
              this link lets the user add members to the initial group.
            */
            a!linkField(
                labelPosition: "COLLAPSED",
                showWhen: not( local!actionTaken),
                links: {
                  a!dynamicLink(
                    label: "+ Add Users to " & group(ri!initialGroup, "groupName"),
                    value: "ADD",
                    saveInto: {
                      ri!btnAction,
                      a!save(ri!addedToGroup, ri!initialGroup)
                    }
                  )
                }
              )
          }
        ),
        a!sectionLayout(
          label: "Add Users to " & group(ri!addedToGroup, "groupName"),
          showWhen: ri!btnAction = "ADD",
          contents: {
            a!pickerFieldUsers(
              label: "Users to Add",
              value: ri!usersToAdd,
              saveInto: a!save(ri!usersToAdd, getdistinctusers(save!value)),
              required: true
            )
          }
        ),
        a!sectionLayout(
          label: "Move User",
          showWhen: ri!btnAction = "MOVE",
          contents: {
            a!richTextDisplayField(
              labelPosition: "COLLAPSED",
              value: {
                "Move ",
                a!richTextItem(
                  text: user(ri!userToMove, "firstName") & " " & user(ri!userToMove, "lastName"),
                  style: "STRONG"
                ),
                " from ",
                a!richTextItem(
                  text: group(ri!moveFromGroup, "groupName"),
                  style: "STRONG"
                ),
                " to"
              },
              readOnly: true
            ),
            a!pickerFieldGroups(
              labelPosition: "COLLAPSED",
              groupFilter: ri!initialGroup,
              value: ri!moveToGroup,
              saveInto: ri!moveToGroup,
              required: true
            )
          }
        ),
        a!buttonLayout(
          showWhen: local!actionTaken,
          primaryButtons: {
            a!buttonWidgetSubmit(
              label: if(ri!btnAction = "ADD", "Add Users", "Move User"),
              style: "PRIMARY",
              disabled: if(
                ri!btnAction = "MOVE",
                or(ri!moveFromGroup = ri!moveToGroup, isnull(ri!moveToGroup)),
                if(isnull(ri!usersToAdd), true, length(ri!usersToAdd) = 0)
              )
            )
          },
          secondaryButtons: {
            /*
              Allows the user to cancel the selected action. If the user 
              cancels out of the action, we need to clear all the 
              selection variables
            */
            a!buttonWidget(
              label: "Cancel",
              style: "NORMAL",
              showWhen: local!actionTaken,
              value: null,
              saveInto: {
                ri!btnAction,
                ri!userToMove,
                ri!userToRemove,
                ri!addedToGroup,
                ri!removeFromGroup,
                ri!moveFromGroup,
                ri!moveToGroup,
                ri!usersToAdd,
                local!selectionValue
              }
            )
          }
        )
      },
      buttons: a!buttonLayout(
        primaryButtons: {
          a!buttonWidgetSubmit(
            label: "Close",
            value: "CLOSE",
            saveInto: ri!btnAction,
            skipValidation: true
          )
        }
      )
    )
  )
)

Notable Expression Details

The comments in the expression above point out many difficult concepts in this recipe. Some of the most important to note are listed below:

  • When managing group membership in process, keep track of the navigation path. This allows you to reinitialize the browser where the user left off, creating the feeling that the user never leaves the form. At the same time, you must be sure that the navigation path does not become invalidated due to altered group membership. In the example above, if a user is removed from a groups membership, it is also removed from the navigation path.
  • To extract the parent of the selected value from the path, use the following expression:
if(
  length(ri!navigationPath) = 1,
  ri!initialGroup,
  togroup(ri!navigationPath[length(ri!navigationPath) - 1])
)
  • If using a user & group browser, use the runtimetypeof() combined with 'type!User' and 'type!Group' to determine what type of value is selected.
  • If using the recipe for a related action, pass the initial group in the related action context and remove the group picker from the interface.

Getting the Interface into a Process Model

To be able move group members, we need to use a process. We will continue this recipe by:

  1. Create a new process model called "Manage Group Members"
  2. Add a new user input task, called "Manage Group Members", connecting it to the start node. Make the connection an activity chain.
    • Set this as a user input task rather than a start form so that you can loop back to it
  3. On the Forms tab, enter the name of your EX_addMoveRemoveUsers interface in the search box and select it
  4. Click Yes when the process modeler asks, "Do you want to import the interface inputs?". This will create node inputs.
  5. On the Data Tab, for each activity class variable, add a duplicate process variable to save the activity class variable.
    • For initialGroup, set the corresponding process variable as the input value
    • For navigationPath, set the corresponding process variable as the input value
  6. Assign the task to the process initiator
  7. At this point, you can connect the input task to the end node to have a functional process. Feel free to do this to test out your interface. None of the buttons will work, but that is coming up next. Before continuing, make sure to undo the testing connection between the input task and the end node.
    1. Next, create a new XOR node below the input task and name it "Do What?" Connect the two with an activity chain.
    2. Next, move the end node to the left of the XOR, and connect the two.

    Your process should now look like this:

    Manage Group Members Process 1

    Now you can start to actually build the nodes that handle the actions.

    1. Create three new sets of nodes, stemming from the gateway. Activity chain all connections going forward.
      • "Add Group Members" - do not change the name
      • "Remove Group Members" - change name to "Remove Group Member"
      • "Add Group Members" connected to a "Remove Group Members" - change the names to "Add Group Member for Move" and "Remove Group Member for Move," respectively
    2. Connect "Add Group Members," "Remove Group Member," and "Remove Group Member for Move" back to "Manage Group Membership," and activity chain the connections

    At this point, your process should look something like this:

    Manage Group Members Process 1

    1. Next, configure the "Do What?" logic
      • If pv!btnAction = "ADD" is True, then go to "Add Group Member"
      • Else If pv!btnAction = "REMOVE" is True, then go to "Remove Group Member"
      • Else If pv!btnAction = "MOVE" is True, then go to "Add Group Member for Move"
      • Else if none are true, go to "End Node"

    Finally, configure the remaining nodes in the process model and publish as an action.

    1. In "Add Group Member"
      • Set "Choose New Members" to pv!usersToAdd
      • Set "Choose Group" to pv!addToGroup
    2. In "Remove Group Member"
      • Set "Choose Members" to pv!userToRemove
      • Set "Group" to pv!removeFromGroup
    3. In "Move to Group"
      • Set "Choose New Members" to pv!userToMove
      • Set "Choose Group" to pv!moveToGroup
    4. In "Move from Group"
      • Set "Choose Members" to pv!userToMove
      • Set "Group" to pv!moveFromGroup
    5. Save & Publish the model
    6. From the Application Designer, go to settings, then Application Actions. Add the process model, "Manage Group Members," as an action called "Manage Group Members."
    7. Publish your application

    Test it Out

    At this point, you are ready to manage your groups membership. To try it out:

    1. Go to the actions tab, click on your new action, Manage Group Members
    2. Click on a group and click Add Members
    3. In the picker, type in names of users, and click Add Users to submit the form. You should be brought back to the interface, and the users should now appear as members of the group.
    4. Click on one of the new users
    5. Click Remove Member. The user should no longer be a member of the group.
    6. Click on another user
    7. Click Move Member.
    8. Click on another group or type a group name in the picker. Click Move User to confirm. The user should now be moved.
17.2
FEEDBACK