Table of Contents
Ticket Widget: Building a Multi-Step Request
Updated
by Helpdocs Admin
This article shows how to build a flow that spans several widget responses, using a Form, a Progress widget, actions, state and form. It is for developers. It assumes you have read the Actions, Form, Progress and Form Results and State Across Steps articles.
What the user sees
- A Form asks: "Approve this laptop request?" with Approve or Reject. The user can press "Decide later" or "Confirm".
- After Confirm, a Progress widget shows provisioning steps and updates itself.
- When it is done, the Progress widget stops updating.
Design in three decisions
- Detect first load with
state. - Record the step in
state. - Tell apart which button was pressed with the URL.
1. Detect first load with state
On first load, state is not sent. In a dynamic language, such as a Node.js server, the property is absent (undefined). In a typed language, such as C#, it is null.
Treat both as first load. Also treat an empty state as first load, in case the UI ever sends an empty object.
const isInitial = !body.state || Object.keys(body.state).length === 0;
var isInitial = request.State is null || request.State.Count == 0;
Don't use form for this. On first load form is empty, but it is also empty when the user presses Back or Skip with nothing selected. That is not a first load.
2. Record the step in state
Use one key, for example step.
- No
step: first load. steppresent: the step of the last response.
Each response sets the next value:
{ "state": { "step": "decision" } }This works with one API route or several.
3. Tell apart the buttons
Leading and secondary actions send whatever the user has selected, as primary does. Only primary requires a selection. So form alone can't show which button was pressed. Put the intent in the URL.
Option A: a route per action (clearest)
api/v1/ticket_widgets/sample_identifier/initial (registered as the widget URL)
api/v1/ticket_widgets/sample_identifier/back
api/v1/ticket_widgets/sample_identifier/skip
api/v1/ticket_widgets/sample_identifier/submit
api/v1/ticket_widgets/sample_identifier/next
Option B: one route with a query parameter
api/v1/ticket_widgets/sample_identifier (initial)
api/v1/ticket_widgets/sample_identifier?action=back
api/v1/ticket_widgets/sample_identifier?action=skip
api/v1/ticket_widgets/sample_identifier?action=submit
api/v1/ticket_widgets/sample_identifier?action=next
The parameter name is up to you (action, session, step). Treat back and skip as snapshots. Act only on submit.
Refresh routing
A refresh of a List, Description or Progress widget sends the current URL again, and the UI adds nothing to form. The URL must show what to return. For refresh modes and intervals, see Refresh Modes.
api/v1/ticket_widgets/sample_identifier/progress
api/v1/ticket_widgets/sample_identifier/progress/:id
api/v1/ticket_widgets/sample_identifier/list
api/v1/ticket_widgets/sample_identifier/list/:id
api/v1/ticket_widgets/sample_identifier/description
or
api/v1/ticket_widgets/sample_identifier?location=progress
api/v1/ticket_widgets/sample_identifier?location=list&id={id}
Keeping answers: two options
The UI keeps its answers while the page stays open. It sets the current field's answer by identifier, replaces it if the user comes back and changes it, and adds new fields as the user moves on. It sends the whole form it holds. A reload clears it.
Option 1: store answers in your API. Save each submitted answer in your database. You don't need to return earlier answers in form.
Option 2: combine the database and form. Read from both. You may put stored answers in the form you return, so they show as selected. While the page stays open, the next request includes what is on screen.
Use Option 1 if the flow must survive a reload. After a reload state is empty, so treat it as first load and restore from storage.
Choosing a storage key
Use ticket plus user. Add user type (contact or agent).
- SQL: a table with a unique key on ticket ID, user type and user ID.
- Document database: a document ID that includes ticket ID, user ID and user type.
- Blob storage: the same key in the path.
Use account plus ticket, or the ticket alone, to let several people contribute to one set of answers. Identify the user from the delegate token (see How the Widget Is Requested).
Walkthrough
- Initial load.
stateabsent. Return the Form andstate: { "step": "decision" }. - "Decide later" (secondary,
?action=skip). Don't act onform. Return the same Form. - "Confirm" (primary,
?action=submit). Readform, act on it, return the Progress widget withstate: { "step": "provisioning" },refreshMode: "timer",refreshIntervalSec: 10. - Timer refresh.
state.stepisprovisioning. Return the current steps. - Done. Return the Progress widget with
state: { "step": "done" }and no timer.
{
"type": "form",
"title": "Laptop request",
"value": {
"type": "single_choice",
"identifier": "decision",
"name": "Approve this laptop request?",
"choices": [
{ "identifier": "approve", "name": "Approve" },
{ "identifier": "reject", "name": "Reject" }
]
},
"state": { "step": "decision" },
"secondaryAction": { "label": "Decide later", "url": "api/v1/ticket_widgets/sample_identifier?action=skip" },
"primaryAction": { "label": "Confirm", "url": "api/v1/ticket_widgets/sample_identifier?action=submit" }
}