uportal-app-framework

Framework for creating uPortal Applications

View on GitHub

Widget types and configuration

Widgets are designed to be flexible - users can accomplish a single task or access a single piece of information, or they can access a collection of related things that will help them accomplish their task.

Widgets can:

Basic widgets

The barebones widget provides an app title, a large icon, and a launch button with configurable text. It’s a simple link to an app or external URL.

basic widget

Basic widget entity files

This code block includes most of the fields needed to configure a widget, but there are additional XML tags (<portlet-definition>) you’ll need to create one from scratch. Widgets are app directory entries, so see also documentation about the app directory.

<title>Enrollment</title>
<name>Enrollment</name>
<fname>enrollment-experience</fname>
<desc>Try out the newly redesigned student enrollment experience</desc>
<parameter>
  <name>mdIcon</name>
  <value>fa-university</value>
</parameter>
<parameter>
  <name>alternativeMaximizedLink</name>
  <value>https://enroll.wisc.edu/</value>
</parameter>
<portlet-preference>
  <name>keywords</name>
  <value>enroll</value>
  <value>enrollment</value>
  <value>SOAR</value>
</portlet-preference>
<portlet-preference>
  <name>content</name>
  <readOnly>false</readOnly>
  <value>
    <![CDATA[
      <p>Access the
        <a href="https://enroll.wisc.edu" target="_blank" rel="noopener noreferrer">student enrollment app</a>.
      </p>
    ]]>
  </value>
</portlet-preference>

The above attributes are all you need to configure a basic widget!

Notes:

Predefined widget types

Advantages of widget types

Widget types provide a predefined standard template that can do a lot more than a basic widget while saving you the trouble of creating a custom design.

How to use

Follow these steps for each of the predefined widget types described in this doc:

  1. Follow the “when to use” guidance to select the widget type that will best suit your needs
  2. Add the appropriate widgetType value to your app’s entity file (see widget type’s sample code)
  3. Add a widgetConfig to your app’s entity file (see widget type’s sample code)

list of links widget

<name>widgetType</name>
<value>list-of-links</value>

Use list-of-links to present 2 to 6 links, dynamically sourced or statically configured.

<portlet-preference>
  <name>widgetType</name>
  <value>list-of-links</value>
</portlet-preference>
<portlet-preference>
  <name>widgetConfig</name>
  <value>
    <![CDATA[{
      "launchText":"Launch talent development",
      "links": [
        {
          "title":"All courses and events",
          "href":"https://www.ohrd.wisc.edu/home/",
          "icon":"fa-at",
          "target":"_blank"
        },
        {
          "title":"My transcript",
          "href":"https://www.ohrd.wisc.edu/ohrdcatalogportal/LearningTranscript/tabid/57/Default.aspx?ctl=login",
          "icon":"fa-envelope-o",
          "target":"_blank"
        }
      ]
    }]]>
  </value>
</portlet-preference>

Preferred: icons are Material Icons, referenced like invert_colors (lowercase, no prefix, underscores in place of spaces. )

Deprecated: icons can be Font Awesome icons, referenced like fa-envelope-o (lowercase, fa- prefix, dashes as is typical for Font Awesome references).

Using both Material and Font Awesome icons together can yield visual misalignment of the link icons.

The list-of-links content can be stored directly in widgetConfig, as above. This allows managing this data in the portlet registry as a portlet-preference.

Alternatively, the portlet publication can declare a URL from which the actual links content will be sourced. For dynamic link content:

  1. Omit the “links” entry in the widgetConfig JSON.
  2. Instead add "getLinksURL": "true" in the widgetConfig JSON.
  3. Configure the widgetURL portlet-preference with the location of the dynamic content.

Example of how the widgetURL should respond (note the content.links path):

{
  "content": {
    "links": [
      {
        "href": "https://public.predev.my.wisc.edu",
        "icon": "fa-clock-o",
        "target": "_blank",
        "title": "predev"
      },
      {
        "href": "https://public.test.my.wisc.edu",
        "icon": "fa-calendar-times-o",
        "target": "_blank",
        "title": "Test"
      },
      {
        "href": "https://public.qa.my.wisc.edu",
        "icon": "fa-calendar-times-o",
        "target": "_blank",
        "title": "QA"
      },
      {
        "href": "https://public.my.wisc.edu",
        "icon": "fa-calendar-times-o",
        "target": "_blank",
        "title": "Production"
      },
      {
        "href": "https://it.wisc.edu/services/myuw",
        "icon": "fa-calendar-times-o",
        "target": "_blank",
        "title": "Learn more & make contact"
      }
    ]
  }
}

search with links widget

<name>widgetType</name>
<value>search-with-links</value>
<portlet-preference>
  <name>widgetType</name>
  <value>search-with-links</value>
</portlet-preference>
<portlet-preference>
  <name>widgetConfig</name>
  <value>
    <![CDATA[{
      "actionURL":"https://rprg.wisc.edu/search/",
      "actionTarget":"_blank",
      "actionParameter":"q",
      "launchText":"Go to resource guide",
      "links":[
        {
          "title":"Get started",
          "href":"https://rprg.wisc.edu/phases/initiate/",
          "icon":"fa-map-o",
          "target":"_blank"
        },
        {
          "title":"Resources",
          "href":"https://rprg.wisc.edu/category/resource/",
          "icon":"fa-th-list",
          "target":"_blank"
        }
      ]
    }]]>
  </value>
</portlet-preference>

rss widget type

rss widget

<name>widgetType</name>
<value>rss</value>

When to use rss

rss widget entity file configuration

<portlet-preference>
    <name>widgetType</name>
    <value>rss</value>
</portlet-preference>
<portlet-preference>
    <name>widgetURL</name>
    <value>/rss-to-json/rssTransform/prop/campus-news</value>
</portlet-preference>
<portlet-preference>
    <name>widgetConfig</name>
    <value>
      <![CDATA[{
        "lim": 4,
        "titleLim": 30,
        "showdate": true,
        "showShowing": true
      }]]>
    </value>
</portlet-preference>

Guidance about rss

Note the additional required value in the entity file:

The rssToJson microservice is a fine way to convert desired RSS feeds into the required JSON representation.

action-items widget type

action items widget

<name>widgetType</name>
<value>action-items</value>

When to use action-items

Use action-items to tell the user how many of specific kinds of things need their action.

For example, a manager who approves time off could see “5 leave requests” in an “Approve time and absences” widget.

Additional action-items entity file configuration

<portlet-preference>
    <name>widgetType</name>
    <value>action-items</value>
</portlet-preference>
<portlet-preference>
    <name>widgetConfig</name>
    <value>
      <![CDATA[{
        "actionItems": [
          {
            "textSingular": "absence request to approve",
            "textPlural": "absence requests to approve",
            "feedUrl": "example/path/to/absence-request-quantity-feed",
            "actionUrl": "example/path/to/approve/absences"
          }
        ]
      }]]>
    </value>
</portlet-preference>

widgetConfig for this widget type contains a single key actionItems keying to an array of objects. Each object in the array should have values for each of four keys.

Guidance about action-items

The feedUrl should return a simple JSON object containing a “quantity” key with an integer for a value. For example:

  {
    "quantity": 5
  }

time-sensitive-content widget type

benefits enrollment widget

<name>widgetType</name>
<value>time-sensitive-content</value>

When to use time-sensitve-content

Note: This is an experimental widget type and is subject to change

Additional time-sensitive-content entity file configuration

<portlet-preference>
    <name>widgetType</name>
    <value>time-sensitive-content</value>
</portlet-preference>
<portlet-preference>
    <name>widgetConfig</name>
    <value>
      <![CDATA[{
        "callsToAction": [
          {
            "activeDateRange": {
              "templateLiveDate": "09-09",
              "takeActionStartDate": "09-11",
              "takeActionEndDate": "09-18T12:00",
              "templateRetireDate": "09-20T10:00"
             },
             "actionName": "Annual benefits enrollment",
             "daysLeftMessage": "to change benefits",
             "lastDayMessage": "Today is the last day to enroll!",
             "actionButton": {
               "url": "https://www.hrs.wisconsin.edu/psp/hrs-fd/EMPLOYEE/HRMS/c/W3EB_MENU.W3EB_ENR_SELECT.GBL",
               "label": "Enroll now"
             },
             "learnMoreUrl": "https://www.wisconsin.edu/abe/",
             "feedbackUrl": ""
           },
           {
             "activeDateRange": {
               "templateLiveDate": "2017-03-09",
               "takeActionStartDate": null,
               "takeActionEndDate": null,
               "templateRetireDate": "2017-03-19"
             },
             "actionName": "Some other call to action",
             "actionButton": {
               "url": "https://www.google.com",
               "label": "Do the thing"
             },
             "learnMoreUrl": "www.google.com",
             "feedbackUrl": "www.google.com"
           }
         ]
      }]]>
    </value>
</portlet-preference>

About time-sensitive-content entity file values

Guidance about time-sensitive-content

Date formatting in time-sensitive-content

Configured dates MUST match one of the following formats:

How to configure the active date range in time-sensitive-content

switch widget type

<portlet-preference>
  <name>widgetType</name>
  <value>switch</value>
</portlet-preference>

When to use switch widget type

Use the switch widget type to dynamically switch between other widget types depending upon the value from an expression on the value read from widgetUrl.

For example, suppose there’s a flag that indicates an employee’s benefits enrollment opportunity status. One value indicates an annual open benefits enrollment group opportunity, another indicates an event-driven personal opportunity, etc. The switch widget type could present a time-sensitive-content widget to employees eligible for the group annual benefit enrollment opportunity and a custom widget to employees with a personal event-driven enrollment opportunity and a basic widget to employees with neither opportunity.

switch widget entity file configuration

In this example, the switch reads JSON from a resource URL out of a staff benefits information portlet. From that JSON it extracts an enrollment flag. Depending upon the enrollment flag, it switches between a custom widget highlighting the employee’s personal benefit enrollment opportunity signalled by the H value of that flag, a time-sensitive-content widget that encourages participation in the annual benefits enrollment opportunity signalled by the O value of that flag, or it becomes a list-of-links widget linking general information about benefits.

<portlet-preference>
    <name>widgetType</name>
    <value>switch</value>
</portlet-preference>
<portlet-preference>
    <name>widgetURL</name>
    <value>/portal/p/university-staff-benefits-statement/exclusive/enrollmentFlag.resource.uP</value>
</portlet-preference>
<portlet-preference>
    <name>widgetConfig</name>
    <value>
      <![CDATA[{
        "path": "report[0].enrollmentFlag",
        "cases": [
          {
            "matchValue": "H",
            "widgetType": "custom"
          },
          {
            "matchValue": "O",
            "widgetType": "time-sensitive-content",
            "widgetConfig": {
              "callsToAction": [
                {
                  "activeDateRange": {
                    "templateLiveDate": "09-09",
                    "takeActionStartDate": "09-11",
                    "takeActionEndDate": "09-18T12:00",
                    "templateRetireDate": "09-20T10:00"
                  },
                  "actionName": "Annual benefits enrollment",
                  "daysLeftMessage": "to change benefits",
                  "lastDayMessage": "Today is the last day to enroll!",
                  "actionButton": {
                    "url":
                      "https://www.hrs.wisconsin.edu/psp/hrs-fd/EMPLOYEE/HRMS/c/W3EB_MENU.W3EB_ENR_SELECT.GBL",
                    "label": "Enroll now"
                  },
                  "learnMoreUrl": "https://www.wisconsin.edu/abe/",
                  "feedbackUrl": ""
                },
                {
                  "activeDateRange": {
                    "templateLiveDate": "2017-03-09",
                    "takeActionStartDate": null,
                    "takeActionEndDate": null,
                    "templateRetireDate": "2017-03-19"
                  },
                  "actionName": "Some other call to action",
                  "actionButton": {
                    "url": "https://www.google.com",
                    "label": "Do the thing"
                  },
                  "learnMoreUrl": "www.google.com",
                  "feedbackUrl": "www.google.com"
                }
              ]
            }
          }
        ],
        "defaultCase": {
          "widgetType": "list-of-links",
          "widgetConfig": {
            "links": [
              {
                "title":"Health benefits",
                "href":"https://www.ohrd.wisc.edu/home/",
                "icon":"fa-at",
                "target":"_blank"
              },
              {
                "title":"Other benefits",
                "href":"https://www.ohrd.wisc.edu/ohrdcatalogportal/LearningTranscript/tabid/57/Default.aspx?ctl=login",
                "icon":"fa-envelope-o",
                "target":"_blank"
              }
            ]
          }
        }
      }]]>
    </value>
</portlet-preference>
<portlet-preference>
  <name>widgetTemplate</name> <!-- used in the `custom` case -->
  <value>
    <![CDATA[
      <div style='margin : 0 10px 0 10px;'>
        <div style='background-color: #EAEAEA; border-radius:4px;padding:10px; margin-top:10px;'>
          <span class='bold display-block left' style='text-align: left; padding-left: 10px; font-size: 14px;'>
            You have a personal event-driven benefit enrollment opportunity.
            <a
              href="https://www.hrs.wisconsin.edu/psp/hrs-fd/EMPLOYEE/HRMS/c/W3EB_MENU.W3EB_ENR_SELECT.GBL"
              target="_blank" rel="noopener noreferrer">Exercise it.</a>
          </span>
        </div>
      </div>
      <launch-button
        data-aria-label='Launch benefit information app'
        data-href='/web/exclusive/university-staff-benefits-statement'
        data-target='_self'
        data-button-text='Launch full app'>
      </launch-button>
    ]]>
  </value>
</portlet-preference>

The keys in widgetConfig for a switch widget are

The defaultCase object is

When a switch widget activates a custom type widget, that custom widget reads its widgetTemplate as per normal. This means that a switch widget can meaningfully support activation of at most one custom type widget.

Other configuration common across widget types

Launch button text

If you provide a widgetConfig with any defined widget type (i.e. not a custom widget) with a value for launchText, it will replace the text of the launch button with the provided value, even for non-widgets. Use sentence case in launch button text.

Read more about the launch button best practices.

Launch button URL

Likewise, launchUrl in widgetConfig will customize the URL that the launch bar links to, but only in the context of the expanded mode widget. This overrides the widget alternativeMaximizedLink, only in the context of rendering the expanded-mode widget.

Therefore specifying both alternativeMaximizedLink and widgetConfig.launchUrl is a way to configure the app directory entry to launch to either of two URLs, the launchUrl in the context of the expanded mode widget and the alternativeMaximizedLink in all other contexts. The alternativeMaximizedLink could even link to the expanded mode widget.

Like launchText, this doesn’t automatically work for custom-type widgets. A custom widget template could implement this feature but is not guaranteed to have done so.

config.launchUrlTarget overrides widget target like how config.launchUrl overrides widget alternativeMaximizedLink.

Neither launchUrl nor launchUrlTarget have effect in option-link type widgets, because such widgets dynamically set the launch URL depending upon the option the user selects.

Widget messaging

See documentation about displaying messages over top of widgets.

Maintenance mode

Widgets in maintenance mode will display a message communicating that the app is unavailable and the widget will be disabled (unclickable).

Place widgets into or out of Maintenance mode live via Portlet Administration. (Edit –> set lifecycle state to Maintenance –> Save). Only Portal Administrators can do this.

However. Entity import will clobber this live change unless the entity also reflects the change, via

Example:

<parameter>
  <name>PortletLifecycleState.inMaintenanceMode</name>
  <value>true</value>
</parameter>

Widgets in maintenance mode will show a default maintenance message

widget showing default maintenance message

unless a custom message is configured in widgetConfig as maintenanceMessage,

<portlet-preference>
  <name>widgetConfig</name>
  <value>
    <![CDATA[{
      "maintenanceMessage" : "Entropy has claimed this app."
    }]]>
  </value>
</portlet-preference>

widget showing custom maintenance message

Custom widgets

Using a JSON service is a great way to have user-focused content in your widgets. Here are the steps you have to take to create your custom JSON-backed widget:

1. widgetURL

This is where we will get the data from (in a JSON format). If your JSON feed lives outside of the portal, you will need to setup a rest proxy for that. Please contact the MyUW team for details and assistance.

<portlet-preference>
  <name>widgetURL</name>
  <value>/portal/p/earnings-statement/max/earningStatements.resource.uP</value>
</portlet-preference>

When your widget is rendered, this service is called via a GET. The returned content is stored in the scope variable content.

2. widgetType

Setting this to custom will enable you to provide your own custom template. Be sure to evaluate the out of the box widget types before creating your own (documentation on those above).

<portlet-preference>
    <name>widgetType</name>
    <value>custom</value>
</portlet-preference>

3. widgetTemplate

This is where the template goes. We suggest using a CDATA tag here.

<portlet-preference>
  <name>widgetTemplate</name>
  <value>
    <![CDATA[
      <div style='margin : 0 10px 0 10px;'>
        <loading-gif data-object='content' data-empty='isEmpty'></loading-gif>
        <ul class='widget-list'>
          <li ng-repeat=\"item in content.report |orderBy: ['-paid.substring(6)','-paid.substring(0,2)'] | limitTo:3\"
              class='center'>
            <a href='/portal/p/earnings-statement/max/earning_statement.pdf.resource.uP?pP_docId=' target='_blank'>
              <i class='fa fa-bank fa-fw'></i>  Statement</a>
          </li>
        </ul>
        <div ng-if='isEmpty' style='padding: 10px; font-size: 14px;'>
          <i class='fa fa-exclamation-triangle fa-3x pull-left' style='color: #b70101;'></i>
          <span style='color: #898989;'>We had a problem finding your statements (or you don't have any).</span>
        </div>
        <div style='background-color: #EAEAEA; border-radius:4px;padding:10px; margin-top:10px;'>
          <span class='bold display-block left' style='text-align: left; padding-left: 10px; font-size: 14px;'>
            See all payroll information for more options:
          </span>
          <ul style='text-align: left;list-style-type: disc; font-size: 12px;'>
            <li>See all pay stubs</li>
            <li>Tax statements</li>
            <li>Update direct deposit</li>
          </ul>
        </div>
      </div>
      <launch-button data-href='/portal/p/earnings-statement'
                     data-target='_self'
                     data-button-text='Launch full app'
                     data-aria-label='Launch payroll information'></launch-button>
    ]]>
  </value>
</portlet-preference>

Accessibility guidance

Creating a custom widget means you’ll miss out on built-in accessibility features, like aria-labels for screen reader users. We recommend using the <launch-button> directive for you widget launch button, and providing a simple but meaningful value for the data-aria-label attribute.

Read more about the launch button best practices.

Note: If you do not use the launch-button directive, please give your launch button a class of “launch-app-button” to ensure it matches other widgets.

4. widgetConfig

The widget config is a JSON object. Please note it has to be valid JSON. We used the <![CDATA[]]> tag so we didn’t have to encode everything.

Currently we only use the evalString to evaluate emptiness. We may add more in the future.

<portlet-preference>
  <name>widgetConfig</name>
  <value><![CDATA[{ "evalString" : "!$scope.content.report || $scope.content.report.length === 0"}]]></value>
</portlet-preference>

By doing just this we were able to generate:

custom widget

remote-content widgets

The remote-content widget type sources a URL for the widget content, allowing generating the widget content server-side.

<portlet-preference>
    <name>widgetType</name>
    <value>remote-content</value>
</portlet-preference>
<portlet-preference>
  <name>widgetURL</name>
  <value>/hrs-integration/widgets/benefit-information.html</value>
</portlet-preference>

While waiting for the asynchronous request to the widgetURL, the remote-content widget type shows a loading indicator.

The remote content should include the widget launch button if appropriate. The remote-content widget template will not provide a launch button except in the error case. The widget will use literally the markup responded from the widgetURL as the widget markup.

If the asynchronous request receives an error response, remote-content falls back on rendering as if it were at basic widget.