LSPS documentation logo
LSPS Documentation
Model

You will design a business model that will distribute work between by two types of users, the customer and the support specialist:

  • Customers create a ticket at any time and answer to ticket comments.
  • Support specialists will:
    • open the ticket,
    • add a comment after the customer does,
    • resolve the ticket,
    • view a list of their and new open tickets.

In addition, tickets will expire if the customer does not add a comment within a period of time.

Prerequisites

  1. Open PDS with a new workspace.
  2. Create the GO-BPMN project ticket-application-model.
  3. Optionally, put the project under version control (you will deal with version control in the next part in detail).

Raising a Trouble Ticket

User story: A customer raises a trouble ticket with an issue.

The customer creates a ticket with details, such as, subject, description, date, and priority.

  1. Create a non-executable common module with the following:
    • a data type definition with the Ticket shared record Define the type of the priority field as the Priority type and create Priority enumeration.
      ticket-raising-dataModel.png
    • an organization definition with a Customer role
  2. Create a non-executable module, raising, for the resources for the ticket raising.
    1. Import the common module.
    2. Create a document definition with a Document available only to users with the Customer role: Set the Access rights expression to isPersonIn(getCurrentPerson(), Customer()) and UI Definition set to new TicketRaiseForm().
      ticket-raising-document.png
      Resulting structure with document details
    3. Create the missing form TicketRaiseForm with the components:
      • Form Layout parent component
      • Text Field with ID subject and Value binding
      • Text Area with ID description and Value binding
      • Single Select List with ID priority, Value binding, and Select items options populated with the expression:
        collect(
          //returns all literals of the Priority enumeration:
          Priority.literals(),
          // Over each priority literal, create a SelectItem:
          { p:Priority -> new SelectItem(value-> p, label -> p.toString())}
        )
        ticket-raising-form.png
        Ticket raising form with the detail of the Priority field in the Properties
      • Button component with the name Create Ticket with the following Click Listener expression:
        //collects input from from, creates a new ticket,
        //and submits the form:
        { e -> 
           new Ticket(
            created -> now(),
            //subject of the new ticket is set to the value
            //in the subject Text Input component:
            subject -> subject.getValue(),
            description -> description.getValue(),
            priority -> priority.getValue()?? Priority.GENERAL as Priority
          );
          Forms.submit() 
        }
  3. Run the PDS Embedded Server and switch to the Management perspective.
    ticket-raising-running.png
    1. Upload the raising module.
      ticket-raising-module-upload.png
    2. Assign the Customer role to a person: make sure to save the changes.
    3. Log in to the Application User Interface (by default, localhost:8080/lsps-application) as the person and create a ticket.
  4. Optionally, open your database client and check that the ticket table contains the new ticket. The embedded database is an H2 database with the user lsps and password lsps.
    ticket-raising-database.png
    Details of embedded database connection and the new ticket table

Summary

You have done the following:

  • You have created the shared record Ticket so that the ticket data is persisted.
  • You have created a customer role so you can work with a particular set of users.
  • You have created document roleTicketDoc accessible only to a customer so that customer users can enter a new ticket as a new instance of the Ticket shared record.
  • You have assigned the customer role to a user.
  • You have uploaded the module with the document.
  • You have create a new ticket as the customer user.

Tips and Takeaways

  • Organize your data in modules so you can reuse them when necessary: In the example, the data type and organization definition are stored in the common module. This will allow you to reuse the definitions in new modules.
  • Define labels for your records and their fields: you can then call getLabel() to get the label, for example, in the caption of a form component and keep labels in your forms consistent. This can be especially helpful when localization of model resources is required.
    ticket-raising-tweaks-label.png
    Label of an enumeration field
    Options expression of the Single-Select List that uses enumeration labels
    collect(
      Priority.literals(),
      { p:Priority -> new SelectItem(value-> p, label -> p.getLabel())}
    )
  • Set a prefix for the tables of your data type definition so you can easily identity its database tables.
    ticket-raising-tweaks-prefix.png
  • Click the Link with Editor arrow in GO-BPMN Explorer so that the file with the currently edited resource is always selected.
    ticket-raising-tweaks-editor-link.png
  • Tweak the form presentation before you resort to custom styling:
    • Expand components in the layout by calling c.setWidthFull() on the Init tab of each input component.
    • Disallow empty selection for the Priority select box by calling c.setNullSelectionAllowed(false).
  • Set the Console view so it gains focus only when an error is raised to prevent it from stealing focus unnecessarily .
    ticket-raising-tweaks-console.png
  • Upload modules from workspace directly from GO-BPMN Explorer: right-click the module and go to Upload As > Model: To upload the most-recently uploaded module again, you can then use the Upload menu button.
    ticket-raising-tweaks-upload.png
  • Display a preview of your form by clicking the Preview button in the form editor: This creates a dummy model instance with your form.
    ticket-raising-tweaks-preview.png
    Note that your user must have a security role with the Form:Preview right to display such previews; You can modify the rights of a security role in the Security Management view in the Management perspective.
    ticket-raising-security-right.png
  • To remove the model instances created by form previews, go to the Management perspective, right-click the Model Instance view and select Remove All Form Preview Model Instances.
    ticket-raising-remove-previews.png

Resolving a Ticket

User story: A support specialist resolves the ticket.

When the customer submits the ticket, the status of the created ticket becomes open and the server creates a to-do for the support specialists.

  1. In the common module:
    1. Add field status of type Status to Ticket and create the Status enumeration. Set the labels for its literals.
      ticket-resolving-data-model.png
    2. In the organization definition, create the SupportSpecialist role.
  2. In the TicketRaiseForm, create the new ticket in the status open: adjust the Click Listener expression of the Create Ticket button:
    { e ->
       def Ticket newTicket := new Ticket(
        created -> now(),
        subject -> subject.getValue(),
        description -> description.getValue(),
        //sets priority to GENERAL if not set:
        priority -> priority.getValue()?? Priority.GENERAL as Priority,
        //added this line:
        status -> Status.OPEN
      );
      Forms.submit()
    }
    
  3. Edit the Click Listener expression further so it creates a model instance; pass the new ticket to the model instance:
    { e ->
       //saving new ticket in the newTicket expression variable:
       def Ticket newTicket := new Ticket(
        created -> now(),
        subject -> subject.getValue(),
        description -> description.getValue(),
        priority -> priority.getValue() as Priority,
        status -> Status.OPEN
      );
      //creating model instance with the newTicket as its process entity
      //(it is passed to the model instance as a special property):
      createModelInstance(
         synchronous -> false,
         //you will create the executable module *process* in the next step:
         model -> getModel("process"),
         processEntity -> newTicket, 
         properties -> null);
      Forms.submit()
    }
  4. Create an executable module named process with the following:
    • raising module import (this will recursively import common)
    • global variable definition with the ticket variable initialized to the processEntity of type Ticket; set its initial value to getProcessEntity(Ticket).
    • BPMN-based process with a User task for support specialists
      ticket-resolving-process.png
  5. In common, create a forms directory with the missing ProcessTicketForm that will display the ticket details:
    • Create the ticket form variable.
    • Store the ticket passed by the User task in the form variable:
      //content of the ProcessTicketForm.methods file:
      ProcessTicketForm {
      //constructor that sets ticket form variable to the passed ticket argument:
        public ProcessTicketForm(Ticket ticket){
           this.ticket := ticket
        }
      }
      
    • Create content:
      • Form Layout with the ticket details
        • Label with Value binding set to ticket.subject
        • Label with Value binding set to ticket.description
        • Label with Value binding set to ticket.priority.getLabel()
      • Button with the listener expression that sets the ticket as resolved and submits the to-do:
        { e ->
            ticket.status := Status.RESOLVED;
            Forms.submit();
        }
        
  6. Upload the process module and test it (note that raising and common are uploaded as well):
    1. Assign the SupportSpecialist role to a person. Make sure to save the changes.
    2. Log in to the Application User Interface as the person with the Customer role; create a ticket from the document.
    3. Log in to the Application User Interface as the person with the SupportSpecialist role and resolve the ticket from the to-do.
    4. In the Management perspective, check the status on the server: a model instance with the ticket as its property was created and executed.
      ticket-resolving-model-instance.png
      Details of the model instance created by the document
  7. Optionally, open your database client and check that the ticket table contains the new ticket.

Tips and Takeaways

  • The details of a model instance might not be always available since it is governed by the Create process log flag of modules. Also the option can be disabled globally on the server to improve performance.
  • Always try to minimize the amount of global variables.
  • To delete the database of the PDS Embedded Server, go to Server Connections> LSPS Embedded Server > Database Management: in the displayed dialog box, click Reset. If the option is disabled, stop the server by clicking Stop first.
  • To delete and recreate the data model whenever you upload a module, set the database strategy for your connection under the Server Connections menu. This sets the default strategy: Mind that it might be overriden in the upload configuration of models.
    ticket-resolving-tweaks-database-strategy.png
    Default upload strategy setting for the embedded server
    schemastragetyonmodelconfig.png
    Upload stategy set on the upload configuration of the process model
  • Check your module-imports schema in the Module Dependency View: To display the view, go to Window > Show View > Module Dependency View.
    intro-moduleDependencyView.png
  • To display the complete expression of your form, in the form editor, right-click anywhere in the form and select Display Widget Expression.

Commenting a Ticket

User story: The customer and support specialist add comments to the ticket.

This means you need to start tracking the customer who raised the ticket so you know whom to ask for further comments as well as the comments and their authors. For the time being, a ticket does not have to be handled by the same support specialist.

  1. Extend the data model in the common module so that Ticket holds information on who created it:
    1. Insert Record Import of human:Person.

      Person represents all users of the LSPS Application.

    2. Create a data relationship between Ticket and Person.
      ticket-comment-person-relationship.png
    3. Generate the index for Ticket to Person ID: right-click the data model root in the Outline view and, in the Properties view, click Generate Indexes.
      ticket-comment-generate-index.png
  2. In raising::TicketRaiseForm, edit Click Listener of the Create Ticket button: set the createdBy record to the user who is requesting the action from the document, that is, the current user:
    { e ->
       //saving new ticket in the newTicket expression variable:
       def Ticket newTicket := new Ticket(
        created -> now(),
        subject -> subject.getValue(),
        description -> description.getValue(),
        priority -> priority.getValue() as Priority,
        status -> Status.OPEN,
        //added this:
        createdBy -> getCurrentPerson()
      );
       ...
    
  3. Extend the data model in the common module further so that Ticket holds its comments and comments hold data about who created them:
    1. Create a Comment shared record.
    2. Create a relationship between Comment and Person: it is sufficient to name the end directed toward Person, since you don't need to know what comments a person made.
    3. Create a relationship between Ticket and Comment: name both ends so you can request comment from a ticket and request the ticket of a particular comment. The end directed toward Comment has the Set multiplicity, since a ticket can have multiple comments.
    4. Generate indexes for the relationship).
      ticket-comment-com-relationship.png
      Relationship end of Comment with ordering defined
  4. Allow commenting in ProcessTicketForm:
    1. Add a Text Area for a new comment with Value binding and ID comment.
    2. Add a Submit button with the click action { e -> Forms.submit(); } so the user can submit the to-do without resolving the ticket.
    3. Add an Add Comment button with listener expression:
      {
         e -> new Comment(
                created -> now(),
                text ->  comment.getValue(),
                ticket -> ticket,
                createdBy -> getCurrentPerson()
                );
             //clears the comment text area:
             comment.setValue("");
      }
  5. Display all comments in ProcessTicketForm:
    1. Add the Repeater component with ID previousComments which will display existing comments:
      1. Select the Vertical option so the instances of the child component are displayed above each other.
      2. Define the data source as Query with the expression getComments(ticket).
      3. Define the iterator type as Comment.
      4. Insert the components that will display comment details into the repeater.
        ticket-comment-repeater.png
        Repeater with the vertical layout as its immediate child
    2. In the common module, create the missing getComments(<Ticket>) query, which returns the comments of a ticket.
      ticket-comment-query.png
    3. Refresh the list of comments when a new comment is created from the click listener of the Add Comment button:
      {
         e -> new Comment(
                created -> now(),
                text ->  comment.getValue(),
                ticket -> ticket,
                createdBy -> getCurrentPerson()
                );
             comment.setValue("");
             //ADDED: refreshes the list of previous comments
             //so they show the new comment:
             previousComments.refresh()
      }
  6. Extend the process that handles the ticket to allow the customer to react to the current comment from support:
    1. Add an Exclusive Gateway between the ProcessTicket task and the Simple End Event.
    2. On the Flow between the gateway and the end event, define the guard expression ticket.status==Status.RESOLVED (the flow will be taken only when the ticket is resolved).
    3. Create a User task and name it CommentTicket.
    4. Set the parameters of the CommentTicket task to:
      title -> "Support has commented your ticket " + ticket.id,
      performers -> { ticket.createdBy },
      uiDefinition -> new ProcessTicketForm(ticket)
    5. Connect the gateway to the CommentTicket task and set the Flow as default.
    6. Connect the CommentTicket task to the ProcessTicket task.
      ticket-comment-process.png
  7. Adapt ProcessTicketForm for the Comment Ticket task: Simply hide or disable the Resolve button if the user does not have the SupportSpecialist role: on the Init tab of the button add the following:
    if not isPersonIn(getCurrentPerson(), SupportSpecialist())
    then
      c.setVisible(false)
    end
  8. Upload the raising and process modules and test the scenario.

Tips and Takeaways

  • To preview parametric forms, such as, the ProcessTicketForm, set the parameter in its run configuration: First run the preview of the form. The preview will fail but the preview configuration is created: Now right-click the form and go to Run As > Run Configurations and edit the Form Expression.
    ticket-comment-form-preview.png
  • Improve presentation of ProcessTicketForm:
    • Use Panels to divide comments and ticket details section;
      ticket-comment-tweaks-insertingparent.png
      Wrapping a component in a container component
    • Select the Spacing option on container components.
    • Add captions.
    • Move buttons to the components related to their actions.
      ticket-comment-form-nicer.png

Validating Ticket Details

User story: The customer must always define a subject and description of their ticket.

  1. In common, define the restrictions for Ticket record fields in constraints.
    ticket-validation-constraints.png
    The length restrictions prevent storing of values which would breach the length allowed by the data model.
  2. Validate the data in the TicketRaiseForm form against the constraints:
    1. Set the id of the Create Ticket button to createButton.
    2. Since you need to validate the Ticket record before it is created, create the new ticket as a proxy record in the Create Ticket listener expression:
      { e ->
          def RecordProxySet recordProxySet := createProxySet(null);
          def Ticket newTicket := recordProxySet.proxy(Ticket);
       
          newTicket.created := now();
          newTicket.subject := subject.getValue();
          newTicket.description := description.getValue();
          newTicket.priority := priority.getValue()?? Priority.GENERAL as Priority;
          newTicket.status := Status.OPEN;
          newTicket.createdBy := getCurrentPerson();
       
          createModelInstance(
      ...
      
    3. Still in the Create Ticket listener expression, after the newTicket proxy is populated with data, collect the messages from the constraints that are violated by the proxy.
      def List<ConstraintViolation> constraintViolations := validate(newTicket, null, null, null);
    4. If the proxy does not violate any constraints, merge its proxy set with recordProxySet.merge(false); and create the process model instance.
    5. If the proxy violates a constraint, display the violations: call showDataErrorMessages(constraintViolations, createButton)

Final click listener expression on the Create Ticket button

{ e ->
   def RecordProxySet recordProxySet := createProxySet(null);
   def Ticket newTicket := recordProxySet.proxy(Ticket);
 
   newTicket.created := now();
   newTicket.subject := subject.getValue();
   newTicket.description := description.getValue();
   newTicket.priority := priority.getValue()?? Priority.GENERAL as Priority;
   newTicket.status := Status.OPEN;
   newTicket.createdBy := getCurrentPerson();
 
  def List<ConstraintViolation> constraintViolations := validate(newTicket, null, null, null);
 
  if constraintViolations.isEmpty() then
    recordProxySet.merge(false);
    createModelInstance(
      synchronous -> false,
      model -> getModel("process"),
      processEntity -> newTicket,
      properties -> null);
    Forms.submit();
  else
    //display violation messages on create button:
    showDataErrorMessages(constraintViolations, createButton)
  end
}

Tips and Takeaways

  • Extract the click listener expression from the Create Ticket button to the TicketRaiseForm.submitTicket() method for better maintenance and call the method from the click listener expression.
    //new click listener expression:
    { e ->
       submitTicket()
    }
  • Make the ticket data in TicketRaiseForm easier to manage:

    1. Create a form variable for the ticket and for a record proxy set.
    2. Initialize them in the form constructor:
      public TicketRaiseForm(){
         recordProxySet := createProxySet(null);
         newTicket := recordProxySet.proxy(Ticket);
      }
    3. Remove their expression variables from the submitTicket() method.

    Resulting TicketRaiseForm constructor and submitTicket() method

    TicketRaiseForm {
     
       public TicketRaiseForm(){
         recordProxySet := createProxySet(null);
         newTicket := recordProxySet.proxy(Ticket);  
       }
       public void submitTicket(){
     
         newTicket.created := now();
         newTicket.subject := subject.getValue();
         newTicket.description := description.getValue();
         newTicket.priority := priority.getValue()?? Priority.GENERAL as Priority;
         newTicket.status := Status.OPEN;
         newTicket.createdBy := getCurrentPerson();
     
         def List<ConstraintViolation> constraintViolations := validate(newTicket, null, null, null);
     
        if constraintViolations.isEmpty() then
          recordProxySet.merge(false);
          createModelInstance(
            synchronous -> false,
            model -> getModel("process"),
            processEntity -> newTicket,
            properties -> null);
          Forms.submit();
        else
          //display violation messages on submitTicket:
          showDataErrorMessages(constraintViolations, submitButton)
        end
       }
    }
    
  • Display the violations on the components that are bound to the record fields with the violation:
    1. Change the binding of the input components to Reference and define its Value as Reference to the respective field of the newTicket variable, for example, &newTicket.subject for the subject input field.
      ticket-validation-tweaks-reference-binding.png
    2. In the submitTicket() method, change the component that displays the constraint messages to the form root: Define the id of the root layout as formLayout and adjust the showDataErrorMessages call. The messages will be displayed on the components bound to the respective record field.
      ...
            createModelInstance(
              synchronous -> false,
              model -> getModel("process"),
              processEntity -> newTicket,
              properties -> null);
            Forms.submit();
          else
            //display violation messages on the form layout or
            //children of the form root for record bindings:
            showDataErrorMessages(constraintViolations, formLayout)
          end
         }
      }
      
  • Analogously, adjust validation in TicketDetailsForm.
    
    //Click listener on the Add Comment button
    //in TicketDetailsForm:
    {
       e ->
          def RecordProxySet recordProxySet := createProxySet(null);
          def Comment newComment := recordProxySet.proxy(Comment);
           
          newComment.created := now();
          newComment.text := comment.getValue();
          newComment.ticket := ticket;
          newComment.createdBy := getCurrentPerson();
          def List<ConstraintViolation> constraintViolations := validate(newComment, null, null, null);
           
          if (constraintViolations.isEmpty()) then
            recordProxySet.merge(false);
            comment.setValue("");
            previousComments.refresh();
          else
            showDataErrorMessages(constraintViolations, comment)
          end
    }

Validation message on the component with constraint violation

ticket-validation-tweaks-error-message.png

Killing an Idle Ticket

User story: If the customer does not respond to a comment of a support specialist within a given period of time, the ticket times out.

  1. Add the ABANDONED status to the Status enumeration.
  2. Adjust the process flow:
    1. Add Timeout Intermediate Event on the boundary of CommentTicket and set its Date property to the point in time when the task should time out, for example, now() + days(3).
    2. Add an outgoing flow to a Simple End Event and change status of the ticket on the flow assignment:
      ticket.status := Status.ABANDONED;
      log("Ticket " + ticket.id + " has been abandoned.", INFO_LEVEL)
      ticket-killing-process.png
  3. Upload the process module and test the scenario when the ticket expires.

Tips and Takeaways

Assigning a Ticket to a Support Specialist

User story: Once a support specialist opens a ticket, it is handled exclusively by them for the rest of the ticket life (Currently, the ProcessTicket task can be handled by any user with the SupportSpecialist role).

  1. In common, adapt the data type model:
    1. Add another relationship of Ticket and Person.
    2. Change the foreign key names for the Ticket table.
    3. Generate indexes.
      ticket-allocation-data.png
  2. Adapt the ticket process: add a user task for support specialists that will allocate the ticket to the person who will deal with this to-do and adjust the rest of the flow.
    ticket-allocation-process.png
  3. Define the allocation on the AllocateTicket task on its Accomplish Assignments tab.
    ticket-allocation-task-assignment.png
  4. Adapt the Performers parameter of the ProcessTicket task to performers /* Set<Performer> */ -> { ticket.allocatedTo }

Tips and Takeaways

  • Consider adding Annotations with comments on the process.
    process_annotations.png

Displaying a List of Tickets to a Support Specialist

User story: A support specialist can view all their open tickets on one page.

  1. In common, create a query getTickets that returns all Tickets for a Person:
    • Name: getTickets
    • Record type: Ticket
    • Iterator: t
    • Parameters: supportSpecialist of type Person
    • Condition: t.allocatedTo == supportSpecialist && t.status == Status.OPEN
    • Static ordering: t.created, DESC
  2. Create a non-executable list module with common module import.
  3. In the list module, create a document for the support specialist.
    ticket-list-document.png
  4. Create the form TicketList as depicted below.
    1. Insert a Grid with ID ticketGrid.
    2. Set the data source to Query with value getTickets(getCurrentPerson()).
    3. Insert Columns with details.
      ticket-list-form.png
  5. Upload the list module and check that the document is available for the users with the correct role.

Adding a Comment At Any Point

User story: The support specialist can view the details and add a comment to their open Tickets at any time.

Allow the support specialist to display details of a ticket from the ticket list and comment on the selected ticket.

  1. Adapt ProcessTicketForm: You need to exclude the Resolve and Submit buttons from the ticket detail. To prevent copying the same code with ticket details, you need to create a new form with the ticket details and reuse this form in ProcessTicketForm:
    1. In the common module, create TicketDetailsForm:
      1. Cut all components from ProcessTicketForm apart from the buttons and insert everything into TicketDetailsForm: use copy-paste.
      2. Create a local variable for the ticket.
      3. Create the parametric constructor with the ticket initialization.
        TicketDetailsForm {
          //constructor that initializes the form variable ticket
          //to the parameter argument:
          public TicketDetailsForm(Ticket ticket){
             this.ticket := ticket
          }
        }
        ticket-details-form.png
  2. Add a Reusable form component to ProcessTicketForm with TicketDetailsForm.
    ticket-details-reused-form.png
  3. Adapt TicketList:
    1. Wrap the Grid of TicketList in a Vertical Layout and add a Vertical Split with ID ticketSplit.
    2. Enable selection in the grid: On the Init tab, call c.setSelectionMode(SelectionMode.SINGLE)
    3. Still on the Init tab of the Grid, define to display the details of the selected ticket as the second component of the split, when the user selects a ticket in the grid.
      c.setSelectionChangeListener({e ->
       
          ticketSplit.setSecondComponent(
             getDetailForm(ticketGrid.getSelection())
          )
        }
      )
  4. Create the getDetailForm() method for the form. Note that selection can be null, either when the form is loaded or when the user unselects a ticket; You will display another form in such a case:
      private FormComponent getDetailForm(List<Object> selection){
     
          if ! selection.isEmpty() then
               (new TicketDetailsForm(selection[0] as Ticket)).getWidget()
            else
               (new EmptyTicketDetailsForm()).getWidget()
            end
      }
    ticket-details-grid.png
  5. In common, create EmptyTicketDetailsForm.

What to do Next

Optionally, go through the agile-pattern tutorial.

Then proceed to Application