|
| 1 | +/** |
| 2 | + * Saving pattern — how BOPS lets caseworkers save their work on a task. |
| 3 | + * |
| 4 | + * Two save actions ("Save and mark as complete" vs "Save changes"), a strong |
| 5 | + * success confirmation, and a return to the task list with the task now |
| 6 | + * showing as Completed. Markup mirrors the real BOPS task stories so the |
| 7 | + * pattern preview reads like the live application. |
| 8 | + */ |
| 9 | +import { mockData, renderStatusTag } from "../../helpers"; |
| 10 | + |
| 11 | +const { application } = mockData; |
| 12 | + |
| 13 | +export default { |
| 14 | + title: "Patterns/Saving pattern", |
| 15 | + parameters: { |
| 16 | + layout: "padded", |
| 17 | + }, |
| 18 | + decorators: [ |
| 19 | + (story) => |
| 20 | + `<div style="max-width: 1100px; margin: 0 auto; padding: 20px;">${story()}</div>`, |
| 21 | + ], |
| 22 | +}; |
| 23 | + |
| 24 | +// --------------------------------------------------------------------------- |
| 25 | +// Wireframe shell — matches the Asking-multiple-things pattern so all pattern |
| 26 | +// previews share the same chrome. |
| 27 | +// --------------------------------------------------------------------------- |
| 28 | + |
| 29 | +function renderShell(innerHtml, { caption } = {}) { |
| 30 | + return ` |
| 31 | + <div class="sp-wireframe"> |
| 32 | + <div class="sp-wireframe__header"> |
| 33 | + <span class="sp-wireframe__logo">GOV.UK</span> |
| 34 | + <span class="sp-wireframe__service">Back Office Planning System</span> |
| 35 | + </div> |
| 36 | + <div class="sp-wireframe__bar"> |
| 37 | + <span>${application.reference}</span> |
| 38 | + <span style="margin: 0 8px; opacity: 0.5;">|</span> |
| 39 | + <span>${application.address}</span> |
| 40 | + </div> |
| 41 | + <div class="sp-wireframe__body"> |
| 42 | + ${innerHtml} |
| 43 | + </div> |
| 44 | + ${caption ? `<div class="sp-wireframe__caption">${caption}</div>` : ""} |
| 45 | + </div> |
| 46 | +
|
| 47 | + <style> |
| 48 | + .sp-wireframe { |
| 49 | + border: 1px solid #b1b4b6; |
| 50 | + border-radius: 6px; |
| 51 | + overflow: hidden; |
| 52 | + font-family: "GDS Transport", arial, sans-serif; |
| 53 | + background: #fff; |
| 54 | + } |
| 55 | + .sp-wireframe__header { |
| 56 | + background: #0b0c0c; |
| 57 | + padding: 10px 20px; |
| 58 | + display: flex; |
| 59 | + align-items: baseline; |
| 60 | + gap: 12px; |
| 61 | + } |
| 62 | + .sp-wireframe__logo { |
| 63 | + color: #fff; |
| 64 | + font-weight: bold; |
| 65 | + font-size: 18px; |
| 66 | + } |
| 67 | + .sp-wireframe__service { |
| 68 | + color: #fff; |
| 69 | + font-size: 16px; |
| 70 | + } |
| 71 | + .sp-wireframe__bar { |
| 72 | + background: #f3f2f1; |
| 73 | + padding: 8px 20px; |
| 74 | + font-size: 14px; |
| 75 | + color: #0b0c0c; |
| 76 | + border-bottom: 1px solid #b1b4b6; |
| 77 | + } |
| 78 | + .sp-wireframe__body { |
| 79 | + padding: 30px 40px 40px; |
| 80 | + } |
| 81 | + .sp-wireframe__caption { |
| 82 | + background: #f3f2f1; |
| 83 | + border-top: 1px solid #b1b4b6; |
| 84 | + padding: 10px 20px; |
| 85 | + font-size: 13px; |
| 86 | + color: #505a5f; |
| 87 | + } |
| 88 | + </style>`; |
| 89 | +} |
| 90 | + |
| 91 | +// --------------------------------------------------------------------------- |
| 92 | +// A compact example of a real BOPS task form — the "Confirm site notice is in |
| 93 | +// place" task. Used as the body above the save buttons so the button variants |
| 94 | +// sit in a realistic context instead of floating on their own. |
| 95 | +// --------------------------------------------------------------------------- |
| 96 | + |
| 97 | +function renderTaskFormBody() { |
| 98 | + return ` |
| 99 | + <h1 class="govuk-heading-l">Confirm site notice is in place</h1> |
| 100 | +
|
| 101 | + <div class="govuk-form-group"> |
| 102 | + <fieldset class="govuk-fieldset" role="group"> |
| 103 | + <legend class="govuk-fieldset__legend govuk-fieldset__legend--s"> |
| 104 | + What date was the site notice displayed? |
| 105 | + </legend> |
| 106 | + <div class="govuk-date-input"> |
| 107 | + <div class="govuk-date-input__item"> |
| 108 | + <div class="govuk-form-group"> |
| 109 | + <label class="govuk-label govuk-date-input__label" for="displayed-day">Day</label> |
| 110 | + <input class="govuk-input govuk-date-input__input govuk-input--width-2" id="displayed-day" name="displayed[day]" type="text" inputmode="numeric" value="12"> |
| 111 | + </div> |
| 112 | + </div> |
| 113 | + <div class="govuk-date-input__item"> |
| 114 | + <div class="govuk-form-group"> |
| 115 | + <label class="govuk-label govuk-date-input__label" for="displayed-month">Month</label> |
| 116 | + <input class="govuk-input govuk-date-input__input govuk-input--width-2" id="displayed-month" name="displayed[month]" type="text" inputmode="numeric" value="04"> |
| 117 | + </div> |
| 118 | + </div> |
| 119 | + <div class="govuk-date-input__item"> |
| 120 | + <div class="govuk-form-group"> |
| 121 | + <label class="govuk-label govuk-date-input__label" for="displayed-year">Year</label> |
| 122 | + <input class="govuk-input govuk-date-input__input govuk-input--width-4" id="displayed-year" name="displayed[year]" type="text" inputmode="numeric" value="2026"> |
| 123 | + </div> |
| 124 | + </div> |
| 125 | + </div> |
| 126 | + </fieldset> |
| 127 | + </div> |
| 128 | +
|
| 129 | + <div class="govuk-form-group"> |
| 130 | + <label class="govuk-label govuk-label--s" for="evidence-upload"> |
| 131 | + Upload evidence of site notice in place |
| 132 | + </label> |
| 133 | + <div class="govuk-hint">Add any photos of the site notice being displayed.</div> |
| 134 | + <input class="govuk-file-upload" id="evidence-upload" name="documents[]" type="file" multiple> |
| 135 | + </div>`; |
| 136 | +} |
| 137 | + |
| 138 | +// --------------------------------------------------------------------------- |
| 139 | +// Story 1 — Save and mark as complete (primary action) |
| 140 | +// --------------------------------------------------------------------------- |
| 141 | + |
| 142 | +export const SaveAndMarkAsComplete = { |
| 143 | + name: "Save and mark as complete (primary)", |
| 144 | + render: () => |
| 145 | + renderShell( |
| 146 | + ` |
| 147 | + <div class="govuk-grid-row"> |
| 148 | + <div class="govuk-grid-column-two-thirds"> |
| 149 | + ${renderTaskFormBody()} |
| 150 | +
|
| 151 | + <div class="govuk-button-group govuk-!-padding-top-4"> |
| 152 | + <button type="submit" class="govuk-button" data-module="govuk-button"> |
| 153 | + Save and mark as complete |
| 154 | + </button> |
| 155 | + <a class="govuk-link" href="#">Back</a> |
| 156 | + </div> |
| 157 | + </div> |
| 158 | + </div> |
| 159 | + `, |
| 160 | + { |
| 161 | + caption: |
| 162 | + "The primary save action on a task form. 'Save and mark as complete' persists the user's work AND changes the task's status to Completed on the task list. Use this when the user is declaring the task done.", |
| 163 | + } |
| 164 | + ), |
| 165 | +}; |
| 166 | + |
| 167 | +// --------------------------------------------------------------------------- |
| 168 | +// Story 2 — Save changes only (secondary action) |
| 169 | +// --------------------------------------------------------------------------- |
| 170 | + |
| 171 | +export const SaveChangesOnly = { |
| 172 | + name: "Save changes (secondary)", |
| 173 | + render: () => |
| 174 | + renderShell( |
| 175 | + ` |
| 176 | + <div class="govuk-grid-row"> |
| 177 | + <div class="govuk-grid-column-two-thirds"> |
| 178 | + ${renderTaskFormBody()} |
| 179 | +
|
| 180 | + <div class="govuk-button-group govuk-!-padding-top-4"> |
| 181 | + <button type="submit" class="govuk-button govuk-button--secondary" data-module="govuk-button"> |
| 182 | + Save changes |
| 183 | + </button> |
| 184 | + <a class="govuk-link" href="#">Back</a> |
| 185 | + </div> |
| 186 | + </div> |
| 187 | + </div> |
| 188 | + `, |
| 189 | + { |
| 190 | + caption: |
| 191 | + "The secondary save action. 'Save changes' persists the user's work without marking the task complete, so they can come back to it later. Use this for in-progress work or when a caseworker is interrupted.", |
| 192 | + } |
| 193 | + ), |
| 194 | +}; |
| 195 | + |
| 196 | +// --------------------------------------------------------------------------- |
| 197 | +// Story 3 — Success confirmation banner |
| 198 | +// --------------------------------------------------------------------------- |
| 199 | + |
| 200 | +export const SuccessBanner = { |
| 201 | + name: "Success confirmation", |
| 202 | + render: () => |
| 203 | + renderShell( |
| 204 | + ` |
| 205 | + <div class="govuk-grid-row"> |
| 206 | + <div class="govuk-grid-column-two-thirds"> |
| 207 | + <div class="govuk-notification-banner govuk-notification-banner--success" role="alert" |
| 208 | + aria-labelledby="govuk-notification-banner-title" |
| 209 | + data-module="govuk-notification-banner"> |
| 210 | + <div class="govuk-notification-banner__header"> |
| 211 | + <h2 class="govuk-notification-banner__title" id="govuk-notification-banner-title"> |
| 212 | + Success |
| 213 | + </h2> |
| 214 | + </div> |
| 215 | + <div class="govuk-notification-banner__content"> |
| 216 | + <h3 class="govuk-notification-banner__heading"> |
| 217 | + Site notice confirmation has been saved and the task has been marked as complete. |
| 218 | + </h3> |
| 219 | + </div> |
| 220 | + </div> |
| 221 | +
|
| 222 | + <h1 class="govuk-heading-l">Consultation</h1> |
| 223 | + <p class="govuk-body">Return to the task list to continue the next task.</p> |
| 224 | + <a class="govuk-link" href="#">Back to application overview</a> |
| 225 | + </div> |
| 226 | + </div> |
| 227 | + `, |
| 228 | + { |
| 229 | + caption: |
| 230 | + "The success banner names exactly what was saved and what state changed. Specific wording beats a generic 'Saved successfully' — it gives caseworkers confidence their work landed and reminds them what they just did.", |
| 231 | + } |
| 232 | + ), |
| 233 | +}; |
| 234 | + |
| 235 | +// --------------------------------------------------------------------------- |
| 236 | +// Story 4 — Return to the task list with the task now Completed |
| 237 | +// --------------------------------------------------------------------------- |
| 238 | + |
| 239 | +function renderConsultationTaskList() { |
| 240 | + const tasks = [ |
| 241 | + { name: "Select consultees", status: "complete" }, |
| 242 | + { name: "Send site notice", status: "complete" }, |
| 243 | + { name: "Confirm site notice is in place", status: "complete" }, |
| 244 | + { name: "Send letters to neighbours", status: "in_progress" }, |
| 245 | + { name: "Create press notice", status: "not_started" }, |
| 246 | + { name: "View consultee responses", status: "not_started" }, |
| 247 | + ]; |
| 248 | + |
| 249 | + const items = tasks |
| 250 | + .map( |
| 251 | + (task) => ` |
| 252 | + <li class="app-task-list__item" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #b1b4b6;"> |
| 253 | + <span class="app-task-list__task-name"><a class="govuk-link" href="#">${task.name}</a></span> |
| 254 | + <span class="app-task-list__task-tag">${renderStatusTag(task.status)}</span> |
| 255 | + </li>` |
| 256 | + ) |
| 257 | + .join(""); |
| 258 | + |
| 259 | + return ` |
| 260 | + <h2 class="govuk-heading-m">Consultation</h2> |
| 261 | + <ul class="app-task-list__items" style="list-style: none; padding: 0; margin: 0;"> |
| 262 | + ${items} |
| 263 | + </ul>`; |
| 264 | +} |
| 265 | + |
| 266 | +export const ReturnToTaskList = { |
| 267 | + name: "Return to task list (what's next)", |
| 268 | + render: () => |
| 269 | + renderShell( |
| 270 | + ` |
| 271 | + <div class="govuk-grid-row"> |
| 272 | + <div class="govuk-grid-column-two-thirds"> |
| 273 | + <span class="govuk-caption-l">${application.reference}</span> |
| 274 | + <h1 class="govuk-heading-l">Application</h1> |
| 275 | +
|
| 276 | + ${renderConsultationTaskList()} |
| 277 | + </div> |
| 278 | + </div> |
| 279 | + `, |
| 280 | + { |
| 281 | + caption: |
| 282 | + "After saving, the user lands back on the task list. The task they just completed now shows a 'Completed' tag, and the next task they could pick up is still visible in context — so the save action flows naturally into the next piece of work.", |
| 283 | + } |
| 284 | + ), |
| 285 | +}; |
0 commit comments