@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
|
-
<
|
|
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.
|
|
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
|
-
|
|
439
|
-
|
|
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
|
|
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} />
|
package/dist/components.md
CHANGED
|
@@ -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.
|
package/dist/registry.d.ts
CHANGED
|
@@ -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) {
|