@reqquest/ui 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,11 +2,11 @@
2
2
  import { Form } from '@txstate-mws/svelte-forms'
3
3
  import MachineLearning from 'carbon-icons-svelte/lib/MachineLearning.svelte'
4
4
  import WarningFilled from 'carbon-icons-svelte/lib/WarningFilled.svelte'
5
- import WatsonHealthStackedMove from 'carbon-icons-svelte/lib/WatsonHealthStackedMove.svelte'
6
5
  import Edit from 'carbon-icons-svelte/lib/Edit.svelte'
6
+ import SidePanelOpenFilled from 'carbon-icons-svelte/lib/SidePanelOpenFilled.svelte'
7
7
  import { enumRequirementStatus, enumRequirementType, PromptIndicators, statusReasonsToFeedback, translateMutations, type PhaseChangeMutations, type StatusReason } from '@reqquest/ui'
8
8
  import { isInlineReviewerEditPrompt, isDisplayablePrompt, hasDisplayablePrompts, RenderDisplayComponent, applicantRequirementTypes, reviewerRequirementTypes, api, PromptSaveQueue, type BasicRequestData } from '$internal'
9
- import { FormInlineNotification, Panel, PanelFormDialog } from '@txstate-mws/carbon-svelte'
9
+ import { FormInlineNotification, Panel, PanelFormDialog, TagSet } from '@txstate-mws/carbon-svelte'
10
10
  import { Tooltip } from 'carbon-components-svelte'
11
11
  import { uiRegistry } from '$local';
12
12
  import { Button } from 'carbon-components-svelte'
@@ -36,6 +36,7 @@
36
36
  type PromptWithExtra = Prompt & PromptExtraData & { allowSaveWithoutChanges?: boolean }
37
37
  let editingPromptWithData: PromptWithExtra | undefined = undefined
38
38
  let promptBeingEdited: Prompt | undefined = undefined
39
+ let editingViaReviewDialog = false
39
40
  let showPromptDialog = false
40
41
  let fetchingEditPrompt = false
41
42
 
@@ -51,10 +52,11 @@
51
52
  saveQueue.adopt(appRequest.dataVersion)
52
53
  }
53
54
 
54
- function editPrompt (prompt: Prompt, allowSaveWithoutChanges: boolean = false) {
55
+ function editPrompt (prompt: Prompt, allowSaveWithoutChanges: boolean = false, viaReviewDialog: boolean = false) {
55
56
  return async () => {
56
57
  if (fetchingEditPrompt) return
57
58
  promptBeingEdited = prompt
59
+ editingViaReviewDialog = viaReviewDialog
58
60
  fetchingEditPrompt = true
59
61
  showPromptDialog = true
60
62
  try {
@@ -88,6 +90,7 @@
88
90
  showPromptDialog = false
89
91
  editingPromptWithData = undefined
90
92
  promptBeingEdited = undefined
93
+ editingViaReviewDialog = false
91
94
  }
92
95
  function onPromptSubmit (prompt: any, modal = false) {
93
96
  return async (data: any) => {
@@ -183,9 +186,28 @@
183
186
  }
184
187
  }
185
188
 
189
+ // answered/total reviewer questions in a section
190
+ function reviewerQuestionTally (section: { requirements: ApplicationRequirement[] }) {
191
+ const questions = new Map<string, ApplicationRequirement['prompts'][number]>()
192
+ for (const requirement of section.requirements) {
193
+ if (!reviewerRequirementTypes.has(requirement.type)) continue
194
+ for (const prompt of requirement.prompts.filter(isDisplayablePrompt)) {
195
+ if (!uiRegistry.getPrompt(prompt.key)?.automation) questions.set(prompt.id, prompt)
196
+ }
197
+ }
198
+ const answered = [...questions.values()].filter(p => p.answered && !p.invalidated).length
199
+ return { answered, total: questions.size }
200
+ }
201
+ $: reviewerQuestionTallies = Object.fromEntries(sections.map(section => [section.key, reviewerQuestionTally(section)]))
202
+
186
203
  $: readyForWorkflow = !application.nextWorkflowStage && application.phase === 'READY_FOR_WORKFLOW' ? lastStageKey : undefined
187
204
  // otherwise the button sits on the last non-stage panel on the page, wherever the layout put it
188
205
  $: latestWorkflow = readyForWorkflow ?? sections.filter(section => section.requirements.every(r => !r.workflowStage?.key)).pop()?.key
206
+ // the one panel "Edit answers" sits on: the stage being reversed onto, or the last review panel when reversing back into review.
207
+ // Requirement statuses don't matter here - a disqualified review must be reversible, and the API decides via actions.reverseWorkflow
208
+ $: reverseTarget = application.previousWorkflowStage
209
+ ? sections.find(section => section.requirements[0]?.workflowStage?.key === application.previousWorkflowStage?.key)?.key
210
+ : sections.filter(section => section.requirements.every(r => !r.workflowStage?.key)).pop()?.key
189
211
 
190
212
  /** inline autoSave forms: refresh statuses/indicators only, never touch the live form */
191
213
  async function onPromptAutoSaved () {
@@ -202,6 +224,18 @@
202
224
  </script>
203
225
  {#each sections as section (section.key)}
204
226
  <Panel title={section.title} expandable expanded>
227
+ <svelte:fragment slot="headerRight">
228
+ {@const tally = reviewerQuestionTallies[section.key]}
229
+ {#if tally?.total}
230
+ <!-- visible pill is hidden from screen readers in favor of the spelled-out text, and the status role announces changes politely on autosave -->
231
+ <span role="status" class="reviewer-question-tally">
232
+ <span aria-hidden="true">
233
+ <TagSet small tags={[{ label: `${tally.answered}/${tally.total} answered`, type: tally.answered === tally.total ? 'green' : 'yellow' }]} />
234
+ </span>
235
+ <span class="bx--visually-hidden">{tally.answered} of {tally.total} questions answered</span>
236
+ </span>
237
+ {/if}
238
+ </svelte:fragment>
205
239
  {#if section.requirements.some(hasDisplayablePrompts)}
206
240
  <dl class="prompts">
207
241
  {#each section.requirements as requirement (requirement.id)}
@@ -210,6 +244,7 @@
210
244
  {@const isReviewerQuestion = reviewerRequirementTypes.has(requirement.type) && !def?.automation}
211
245
  {@const isAutomation = !!def?.automation}
212
246
  {@const editMode = isInlineReviewerEditPrompt(def, requirement.type, prompt)}
247
+ {@const opensReviewDialog = isReviewerQuestion && def?.formMode === 'full'}
213
248
  {@const small = editMode && def?.formMode !== 'full' ? def?.formMode !== 'large' : def?.displayMode !== 'large'}
214
249
  {@const large = editMode && def?.formMode !== 'full' ? def?.formMode === 'large' : def?.displayMode === 'large'}
215
250
  {@const dtid = `dt-title-${prompt.id}`}
@@ -219,13 +254,15 @@
219
254
  {#if prompt.actions.update}
220
255
  {#if prompt.invalidated && !applicantRequirementTypes.has(requirement.type)}
221
256
  <Button kind="primary" size="field" class="prompt-edit mr-2" icon={Review} iconDescription="Review corrections" id="edit-btn-{prompt.id}" aria-labelledby="edit-btn-{prompt.id} {dtid}" on:click={editPrompt(prompt, true)} />
257
+ {:else if opensReviewDialog}
258
+ <Button kind="secondary" size="field" icon={SidePanelOpenFilled} class="prompt-dialog-open" id="edit-btn-{prompt.id}" aria-labelledby="edit-btn-{prompt.id} {dtid}" on:click={editPrompt(prompt, false, true)}>{uiRegistry.reviewerDialogButtonText(prompt.key)}</Button>
222
259
  {:else}
223
260
  <Button kind="ghost" size="field" icon={Edit} iconDescription="Edit" class="prompt-edit" id="edit-btn-{prompt.id}" aria-labelledby="edit-btn-{prompt.id} {dtid}" on:click={editPrompt(prompt)} />
224
261
  {/if}
225
262
  {/if}
226
263
  {/snippet}
227
264
  <dt class:small class:large class:isReviewerQuestion class:disabled={disabled} class:bg-tagyellow-200={isAutomation}>
228
- {#if promptIndicator[prompt.key]?.indicator}
265
+ {#if promptIndicator[prompt.key]?.indicator && (promptIndicator[prompt.key].indicator !== PromptIndicators.PENDING || editMode)}
229
266
  <div class="indicator-tooltip">
230
267
  <Tooltip align="start" direction="bottom">
231
268
  <svelte:fragment slot="icon">
@@ -236,7 +273,9 @@
236
273
  {:else if promptIndicator[prompt.key]?.indicator === PromptIndicators.DISQUALIFYING}
237
274
  <WarningFilled size={20} class="disqualifying-icon" style="fill: var(--cds-support-01, #da1e28)" />
238
275
  {:else if promptIndicator[prompt.key]?.indicator === PromptIndicators.PENDING}
239
- <WatsonHealthStackedMove size={20} class="pending-icon" style="fill: var(--cds-support-04, #0043ce)" />
276
+ <svg class="pending-dot" xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
277
+ <circle cx="4" cy="4" r="4" />
278
+ </svg>
240
279
  {/if}
241
280
  </svelte:fragment>
242
281
  {promptIndicator[prompt.key]?.reason}
@@ -291,7 +330,7 @@
291
330
  <div class="flex justify-end mt-8">
292
331
  {#if application.actions?.advanceWorkflow && (application.workflowStage?.key ? application.workflowStage?.key === section.requirements[0]?.workflowStage?.key : section.key === latestWorkflow)}
293
332
  <Button size="small" on:click={advanceWorkflow}>{'Send to ' + (application.nextWorkflowStage?.title ?? (!application.workflowStage?.blocking ? 'Complete' : 'Review Complete'))}</Button>
294
- {:else if application.actions?.reverseWorkflow && section.requirements.every(r => r.status === enumRequirementStatus.MET || r.status === enumRequirementStatus.NOT_APPLICABLE) && application.previousWorkflowStage?.key === section.requirements[0]?.workflowStage?.key}
333
+ {:else if application.actions?.reverseWorkflow && section.key === reverseTarget}
295
334
  <Button kind='secondary' size="small" on:click={reverseWorkflow}>Edit answers</Button>
296
335
  {/if}
297
336
  </div>
@@ -303,7 +342,7 @@
303
342
  {@const formMode = uiRegistry.getPrompt(promptBeingEdited.key)?.formMode === 'full' ? 'large' : undefined}
304
343
  <PanelFormDialog
305
344
  id={editDialogFormId}
306
- title={editingPromptWithData?.invalidated ? `Review correction "${editingPromptWithData?.title}"` : 'Edit Prompt'}
345
+ title={editingPromptWithData?.invalidated ? `Review correction "${editingPromptWithData?.title}"` : editingViaReviewDialog ? uiRegistry.reviewerDialogButtonText(promptBeingEdited.key) : 'Edit Prompt'}
307
346
  bind:open={showPromptDialog}
308
347
  on:cancel={closePromptDialog}
309
348
  submit={onPromptSubmit(editingPromptWithData, true)}
@@ -419,6 +458,28 @@
419
458
  right: 0;
420
459
  margin: 0;
421
460
  }
461
+ .prompts dt .dt-title {
462
+ display: flex;
463
+ align-items: center;
464
+ gap: 4px;
465
+ min-width: 0;
466
+ }
467
+ /* a large prompt's open-in-dialog button sits at the start of the answer column, so the dt borrows
468
+ the prompt list's column tracks */
469
+ .prompts dt.large:has(> :global(.prompt-dialog-open)) {
470
+ display: grid;
471
+ grid-template-columns: subgrid;
472
+ padding: 0;
473
+ }
474
+ .prompts dt.large:has(> :global(.prompt-dialog-open)) > .dt-title {
475
+ padding: 1rem 15px 1rem 32px;
476
+ }
477
+ .prompts dt.large > :global(.bx--btn.prompt-dialog-open) {
478
+ grid-column: 2;
479
+ justify-self: start;
480
+ align-self: center;
481
+ margin: 0.5rem 15px;
482
+ }
422
483
  .prompts dt .indicator-tooltip {
423
484
  display: inline-block;
424
485
  margin-left: -24px;
@@ -435,8 +496,11 @@
435
496
  :global(.disqualifying-icon) {
436
497
  fill: var(--cds-support-01, #da1e28);
437
498
  }
438
- :global(.pending-icon) {
439
- fill: var(--cds-support-04, #0043ce);
499
+ .pending-dot {
500
+ width: 8px;
501
+ height: 8px;
502
+ aspect-ratio: 1/1;
503
+ fill: var(--cds-support-01, #da1e28);
440
504
  }
441
505
  :global(.panel:has(.isReviewerQuestion.disabled) .panel-header) {
442
506
  background-color: #8C8C8C !important;
@@ -30,7 +30,7 @@
30
30
  })
31
31
  </script>
32
32
 
33
- <ProgressNavContainer title="Your potential benefits" subtitle={noProgramsToList ? (uiRegistry.config.applicantNoProgramsMessage ?? DEFAULT_APPLICANT_NO_PROGRAMS_MESSAGE) : 'Based on your responses so far, you may be eligible for the benefits listed below. If you believe you qualify for additional benefits, you may need to review your answers, complete additional questions, or provides supporting documentation before eligibility can be confirmed.'}>
33
+ <ProgressNavContainer title="Your potential benefits" subtitle={noProgramsToList ? (uiRegistry.config.applicantNoProgramsMessage ?? DEFAULT_APPLICANT_NO_PROGRAMS_MESSAGE) : 'Based on your responses so far, you may be eligible for the benefits listed below. If you believe you qualify for additional benefits, you may need to review your answers, complete additional questions, or provide supporting documentation before eligibility can be confirmed.'}>
34
34
  <div class="max-w-screen-md mx-auto">
35
35
  {#if eligibleApplicationsForNav.length > 0}
36
36
  <ApplicantProgramList applications={eligibleApplicationsForNav} appRequest={appRequestForExport} />
@@ -194,6 +194,11 @@ export const uiRegistry = new UIRegistry(config)
194
194
  `'small'` is about 8 lines at 320px; `'large'` about 32 lines at 800px; `'full'` is a full-screen
195
195
  modal, which also **disables autosave in the reviewer UI** and is what supplies `appRequestId` to
196
196
  the reviewer form. All three must stay responsive down to 320px.
197
+ - **`reviewerDialogButtonText`** `string` (default `'Review'`) - a reviewer question registered with
198
+ `formMode: 'full'` gets a labeled button on the reviewer screen that opens its form in the
199
+ full-height side dialog. This is that button's text and the dialog's title; name what the reviewer
200
+ will look at, e.g. `'Review course information'`. Automation prompts and applicant prompts keep the
201
+ edit pencil. The global fallback of the same setting lives on `UIConfig`.
197
202
  - **`displayMode`** `'small' | 'large'` (default `'small'`) - there is deliberately no `'full'`,
198
203
  because the whole prompt list has to print on one page. Use `'large'` together with
199
204
  `PromptDisplayGrid` when one prompt collects several question/answer pairs.
@@ -93,6 +93,15 @@ export interface PromptDefinition {
93
93
  * Defaults to 'small'.
94
94
  */
95
95
  formMode?: 'small' | 'large' | 'full';
96
+ /**
97
+ * Text of the button on the reviewer screen that opens this prompt's form in the full-height side
98
+ * dialog, also used as that dialog's title. Only applies to reviewer questions registered with
99
+ * `formMode: 'full'`; automation prompts and applicant prompts keep the edit pencil. Name what the
100
+ * reviewer will look at, e.g. "Review course information".
101
+ *
102
+ * Falls back to `UIConfig.reviewerDialogButtonText`, then to "Review".
103
+ */
104
+ reviewerDialogButtonText?: string;
96
105
  /**
97
106
  * Set this to true to make the prompt visually distinct as something that is meant to
98
107
  * be filled in by an automation. If you provide a formComponent, humans with permission
@@ -307,6 +316,13 @@ export interface UIConfig<PK extends string = PromptKey, RK extends string = Req
307
316
  * render nothing anywhere and handle the `statusReasons` prop inside your own form components.
308
317
  */
309
318
  statusReasonNotifications?: StatusReasonNotificationConfig;
319
+ /**
320
+ * Global layer of `PromptDefinition.reviewerDialogButtonText`: the text of the reviewer screen's button
321
+ * that opens a `formMode: 'full'` reviewer question in the full-height side dialog, and that dialog's
322
+ * title. A prompt's own value
323
+ * overrides this one. Defaults to "Review".
324
+ */
325
+ reviewerDialogButtonText?: string;
310
326
  /**
311
327
  * Applicant Review submission page title and subtitle text.
312
328
  */
@@ -434,6 +450,7 @@ export declare class UIRegistry {
434
450
  getWord(key: keyof Terminologies, count?: number, inclusive?: boolean): string;
435
451
  getPlural(key: keyof Terminologies): string;
436
452
  getPrompt(key: string): PromptDefinition | undefined;
453
+ reviewerDialogButtonText(key: string): string;
437
454
  protected statusReasonStatusCache: Map<string, Set<RequirementStatus>>;
438
455
  statusReasonNotificationStatuses(key: string, audience: StatusReasonAudience): Set<RequirementStatus>;
439
456
  getRequirement(key: string): RequirementDefinition | undefined;
package/dist/registry.js CHANGED
@@ -104,6 +104,9 @@ export class UIRegistry {
104
104
  getPrompt(key) {
105
105
  return this.warnIfMissing('prompt', key, this.promptMap[key]);
106
106
  }
107
+ reviewerDialogButtonText(key) {
108
+ return this.promptMap[key]?.reviewerDialogButtonText ?? this.config.reviewerDialogButtonText ?? 'Review';
109
+ }
107
110
  statusReasonStatusCache = new Map();
108
111
  // The statuses whose requirement reasons the framework renders below this prompt's edit form for the given
109
112
  statusReasonNotificationStatuses(key, audience) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reqquest/ui",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "overrides": {
5
5
  "esrap": "2.3.2"
6
6
  },