@reqquest/ui 1.17.0 → 1.18.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.
Files changed (100) hide show
  1. package/app/env.template +5 -0
  2. package/app/runtime/apply-env.sh +10 -0
  3. package/app/runtime/nginx.conf +37 -0
  4. package/app/src/app.css +21 -0
  5. package/app/src/app.d.ts +13 -0
  6. package/app/src/app.html +12 -0
  7. package/app/src/internal/api.ts +1707 -0
  8. package/app/src/internal/appreq-utils.ts +33 -0
  9. package/app/src/internal/components/AnnouncementBanner.svelte +47 -0
  10. package/app/src/internal/components/AppRequestCard.svelte +173 -0
  11. package/app/src/internal/components/ApplicantOptOutModal.svelte +78 -0
  12. package/app/src/internal/components/ApplicantProgramList.svelte +276 -0
  13. package/app/src/internal/components/ApplicantProgramListTooltip.svelte +44 -0
  14. package/app/src/internal/components/ApplicantPromptPage.svelte +136 -0
  15. package/app/src/internal/components/ApplicantPromptSkeleton.svelte +12 -0
  16. package/app/src/internal/components/ApplicationDetailsView.svelte +369 -0
  17. package/app/src/internal/components/ButtonLoadingIcon.svelte +28 -0
  18. package/{dist → app/src/internal}/components/DelayedSkeleton.svelte +2 -2
  19. package/app/src/internal/components/FieldNestedMultiselect.svelte +270 -0
  20. package/app/src/internal/components/IntroPanel.svelte +41 -0
  21. package/app/src/internal/components/MissingDefinitionNotification.svelte +20 -0
  22. package/app/src/internal/components/PeriodPanel.svelte +125 -0
  23. package/app/src/internal/components/ProgramStatusCell.svelte +134 -0
  24. package/app/src/internal/components/RenderDisplayComponent.svelte +42 -0
  25. package/app/src/internal/components/ReviewerActions.svelte +171 -0
  26. package/app/src/internal/components/ReviewerQuestions.svelte +444 -0
  27. package/app/src/internal/components/StatusMessageList.svelte +54 -0
  28. package/app/src/internal/components/SystemDeterminationNotice.svelte +37 -0
  29. package/app/src/internal/components/TabLinks.svelte +265 -0
  30. package/app/src/internal/components/WarningIconYellow.svelte +22 -0
  31. package/app/src/internal/components/index.ts +19 -0
  32. package/app/src/internal/csv.ts +30 -0
  33. package/app/src/internal/index.ts +8 -0
  34. package/app/src/internal/prompt-save-queue.ts +91 -0
  35. package/app/src/internal/prompt-utils.ts +40 -0
  36. package/app/src/internal/status-utils.ts +503 -0
  37. package/app/src/internal/stores/IStateStore.ts +5 -0
  38. package/app/src/internal/util.ts +27 -0
  39. package/app/src/routes/+error.svelte +4 -0
  40. package/app/src/routes/+layout.svelte +134 -0
  41. package/app/src/routes/+layout.ts +18 -0
  42. package/app/src/routes/+page.ts +14 -0
  43. package/app/src/routes/announcement/+page.svelte +164 -0
  44. package/app/src/routes/announcement/+page.ts +16 -0
  45. package/app/src/routes/dashboards/applicant/+page.svelte +494 -0
  46. package/app/src/routes/dashboards/applicant/+page.ts +95 -0
  47. package/app/src/routes/dashboards/reviewer/+page.svelte +143 -0
  48. package/app/src/routes/dashboards/reviewer/+page.ts +43 -0
  49. package/app/src/routes/periods/+page.svelte +158 -0
  50. package/app/src/routes/periods/+page.ts +13 -0
  51. package/app/src/routes/periods/[id]/configure/+page.svelte +150 -0
  52. package/app/src/routes/periods/[id]/configure/+page.ts +10 -0
  53. package/app/src/routes/requests/+page.svelte +246 -0
  54. package/app/src/routes/requests/+page.ts +24 -0
  55. package/app/src/routes/requests/[id]/+layout.ts +11 -0
  56. package/app/src/routes/requests/[id]/accept/+layout.svelte +80 -0
  57. package/app/src/routes/requests/[id]/accept/+layout.ts +12 -0
  58. package/app/src/routes/requests/[id]/accept/+page.ts +13 -0
  59. package/app/src/routes/requests/[id]/accept/[promptId]/+page.svelte +7 -0
  60. package/app/src/routes/requests/[id]/accept/[promptId]/+page.ts +10 -0
  61. package/app/src/routes/requests/[id]/accept/review/+page.svelte +87 -0
  62. package/app/src/routes/requests/[id]/apply/+layout.svelte +181 -0
  63. package/app/src/routes/requests/[id]/apply/+layout.ts +12 -0
  64. package/app/src/routes/requests/[id]/apply/+page.ts +14 -0
  65. package/app/src/routes/requests/[id]/apply/ProgressNavContainer.svelte +42 -0
  66. package/app/src/routes/requests/[id]/apply/[promptId]/+page.svelte +23 -0
  67. package/app/src/routes/requests/[id]/apply/[promptId]/+page.ts +11 -0
  68. package/app/src/routes/requests/[id]/apply/programs/+page.svelte +71 -0
  69. package/app/src/routes/requests/[id]/apply/review/+page.svelte +102 -0
  70. package/app/src/routes/requests/[id]/approve/+layout.svelte +114 -0
  71. package/app/src/routes/requests/[id]/approve/+page.ts +14 -0
  72. package/app/src/routes/requests/[id]/approve/ApproveLayout.svelte +164 -0
  73. package/app/src/routes/requests/[id]/approve/[programKey]/+page.svelte +396 -0
  74. package/app/src/routes/requests/[id]/approve/[programKey]/+page.ts +15 -0
  75. package/app/src/routes/requests/[id]/approve/activity/+page.svelte +66 -0
  76. package/app/src/routes/requests/[id]/approve/activity/+page.ts +12 -0
  77. package/app/src/routes/requests/[id]/approve/export/+page.ts +12 -0
  78. package/app/src/routes/requests/[id]/approve/export/+page@[id].svelte +41 -0
  79. package/app/src/routes/requests/[id]/export/+page.svelte +32 -0
  80. package/app/src/routes/requests/[id]/export/+page.ts +12 -0
  81. package/app/src/routes/roles/+layout.svelte +16 -0
  82. package/app/src/routes/roles/+layout.ts +14 -0
  83. package/app/src/routes/roles/+page.svelte +225 -0
  84. package/app/src/routes/roles/+page.ts +13 -0
  85. package/app/src/routes/roles/[id]/+layout.ts +10 -0
  86. package/app/src/routes/roles/[id]/+page.svelte +335 -0
  87. package/app/src/routes/roles/[id]/+page.ts +10 -0
  88. package/app/src/routes/roles/[id]/ControlWithTooltip.svelte +15 -0
  89. package/app/src/routes/roles/[id]/TagsWithTooltip.svelte +42 -0
  90. package/app/src/routes/roles/users/+layout.ts +13 -0
  91. package/app/src/routes/roles/users/+page.svelte +46 -0
  92. package/app/src/routes/roles/users/+page.ts +90 -0
  93. package/app/static/favicon.png +0 -0
  94. package/bin/reqquest-ui.js +117 -0
  95. package/cli/config.js +105 -0
  96. package/cli/sync.js +296 -0
  97. package/cli/util.js +29 -0
  98. package/dist/components.md +0 -4
  99. package/package.json +29 -15
  100. package/dist/components/DelayedSkeleton.svelte.d.ts +0 -21
@@ -0,0 +1,171 @@
1
+ <script lang="ts">
2
+ import { toasts } from '@txstate-mws/svelte-components'
3
+ import { Loading } from "carbon-components-svelte"
4
+ import { Button, Select, SelectItem } from 'carbon-components-svelte'
5
+ import { InfoCard, translateMutations } from '@reqquest/ui'
6
+ import { phaseChangeMutations, type PhaseChangeMutations } from '@reqquest/ui'
7
+ import { invalidateAll } from '$app/navigation'
8
+ import { api, type BasicRequestData } from '$internal/api'
9
+ import type { PageData } from '../../routes/requests/[id]/approve/[programKey]/$types'
10
+ import { uiRegistry } from '$local'
11
+ import DocumentExport from 'carbon-icons-svelte/lib/DocumentExport.svelte'
12
+
13
+
14
+ export let application: PageData['appRequest']['applications'][0]
15
+ export let basicRequestData: BasicRequestData
16
+ export let requestId: string
17
+
18
+ $: loading = false
19
+
20
+
21
+
22
+ let appRequestAction: '' | PhaseChangeMutations | 'reopen' | 'close' = ''
23
+ async function onAppRequestAction () {
24
+ loading = true
25
+ if ((phaseChangeMutations as readonly string[]).includes(appRequestAction)) {
26
+ await appRequestPhaseChange(appRequestAction as PhaseChangeMutations)
27
+ } else if (appRequestAction === 'close') {
28
+ await closeRequest()
29
+ } else if (appRequestAction === 'reopen') {
30
+ await reopenRequest()
31
+ }
32
+ appRequestAction = ''
33
+ loading = false
34
+ }
35
+
36
+ async function appRequestPhaseChange (action: PhaseChangeMutations) {
37
+ const response = await api.appRequestPhaseChange(requestId, action)
38
+ await invalidateAll()
39
+ loading = false
40
+ if (!response.success) {
41
+ toasts.add({
42
+ type: 'error',
43
+ title: 'Action Failed',
44
+ message: response.messages.map(m => m.message).join('\n') || 'An unknown error occurred.'
45
+ })
46
+ } else {
47
+ toasts.add({
48
+ type: 'success',
49
+ message: `Successfully ${translateMutations[action]}.`
50
+ })
51
+ }
52
+ await invalidateAll()
53
+ }
54
+
55
+ async function closeRequest () {
56
+ const response = await api.closeAppRequest(requestId)
57
+ await invalidateAll()
58
+ if (!response.success) {
59
+ toasts.add({
60
+ type: 'error',
61
+ title: `Could not close ${uiRegistry.getWord('appRequest').toLowerCase()}`,
62
+ message: response.messages.map(m => m.message).join('\n') || 'An unknown error occurred.'
63
+ })
64
+ } else {
65
+ toasts.add({
66
+ type: 'success',
67
+ message: `${uiRegistry.getWord('appRequest')} closed.`
68
+ })
69
+ }
70
+
71
+ }
72
+
73
+ async function reopenRequest () {
74
+ const response = await api.reopenAppRequest(requestId)
75
+ await invalidateAll()
76
+ if (!response.success) {
77
+ toasts.add({
78
+ type: 'error',
79
+ title: `Could not reopen ${uiRegistry.getWord('appRequest').toLowerCase()}`,
80
+ message: response.messages.map(m => m.message).join('\n') || 'An unknown error occurred.'
81
+ })
82
+ } else {
83
+ toasts.add({
84
+ type: 'success',
85
+ message: `${uiRegistry.getWord('appRequest')} reopened.`
86
+ })
87
+ }
88
+ }
89
+
90
+ let appAction: '' | 'advanceWorkflow' | 'reverseWorkflow' = ''
91
+ async function onAppAction () {
92
+ if (appAction === 'advanceWorkflow') {
93
+ await advanceWorkflow()
94
+ } else if (appAction === 'reverseWorkflow') {
95
+ await reverseWorkflow()
96
+ }
97
+ }
98
+
99
+ async function advanceWorkflow () {
100
+ loading = true
101
+ const response = await api.advanceWorkflow(application.id)
102
+ await invalidateAll()
103
+ loading = false
104
+ if (!response.success) {
105
+ toasts.add({
106
+ type: 'error',
107
+ title: 'Could not advance application',
108
+ message: response.messages.map(m => m.message).join('\n') || 'An unknown error occurred.'
109
+ })
110
+ } else {
111
+ toasts.add({
112
+ type: 'success',
113
+ message: 'Application advanced.'
114
+ })
115
+ }
116
+ }
117
+
118
+ async function reverseWorkflow () {
119
+ loading = true
120
+ const response = await api.reverseWorkflow(application.id)
121
+ await invalidateAll()
122
+ loading = false
123
+ if (!response.success) {
124
+ toasts.add({
125
+ type: 'error',
126
+ title: 'Could not reverse application workflow',
127
+ message: response.messages.map(m => m.message).join('\n') || 'An unknown error occurred.'
128
+ })
129
+ } else {
130
+ toasts.add({
131
+ type: 'success',
132
+ message: 'Application workflow reversed.'
133
+ })
134
+ }
135
+ }
136
+ </script>
137
+
138
+ {#if loading}
139
+ <Loading />
140
+ {/if}
141
+
142
+ <InfoCard title='Reviewer Actions'>
143
+ <div slot='header-right'>
144
+ <Button aria-label='export' href={`/requests/${requestId}/approve/export`} kind="secondary" size="small" icon={DocumentExport} class="ml-[32px]" />
145
+ </div>
146
+ <div class="flow">
147
+ <!-- <Select bind:selected={appAction} labelText="Next step" size="sm">
148
+ <SelectItem value="" text="Choose one" />
149
+ <SelectItem disabled={!application.actions.advanceWorkflow} value="advanceWorkflow" text={'Send to ' + (application.nextWorkflowStage?.title ?? (!application.workflowStage?.blocking ? 'Complete' : 'Review Complete'))} />
150
+ <SelectItem disabled={!application.actions.reverseWorkflow} value="reverseWorkflow" text={'Return to ' + (application.previousWorkflowStage?.title ?? 'Review')} />
151
+ </Select>
152
+ <Button disabled={!appAction} on:click={onAppAction} size="small" class="[ w-full ]">Update program</Button> -->
153
+
154
+ {#if basicRequestData}
155
+ <Select bind:selected={appRequestAction} labelText={`${uiRegistry.getWord('appRequest')} action`} size="sm">
156
+ <SelectItem value="" text="Choose one" />
157
+ <SelectItem disabled={!basicRequestData.actions.completeReview} value="completeReview" text="Complete Review" />
158
+ <SelectItem disabled={!basicRequestData.actions.completeRequest} value="completeRequest" text="Complete Request" />
159
+ <SelectItem disabled={!basicRequestData.actions.reopen} value="reopen" text="Reopen Request" />
160
+ <SelectItem disabled={!basicRequestData.actions.close} value="close" text="Close Request" />
161
+ <SelectItem disabled={!basicRequestData.actions.returnToApplicant} value="returnToApplicant" text="Return To Applicant" />
162
+ <SelectItem disabled={!basicRequestData.actions.returnToOffer} value="returnToOffer" text="Return To Offer" />
163
+ <SelectItem disabled={!basicRequestData.actions.returnToReview} value="returnToReview" text="Return To Review" />
164
+ <SelectItem disabled={!basicRequestData.actions.returnToNonBlocking} value="returnToNonBlocking" text="Return To Final Workflow Tasks" />
165
+ <SelectItem disabled={!basicRequestData.actions.submit} value="submitAppRequest" text="Submit On Behalf Of Applicant" />
166
+ <SelectItem disabled={!basicRequestData.actions.acceptOffer} value="acceptOffer" text="Accept Offer On Behalf Of Applicant" />
167
+ </Select>
168
+ <Button disabled={!appRequestAction} on:click={onAppRequestAction} size="small" class="[ w-full ]">Update application</Button>
169
+ {/if}
170
+ </div>
171
+ </InfoCard>
@@ -0,0 +1,444 @@
1
+ <script lang="ts">
2
+ import { Form } from '@txstate-mws/svelte-forms'
3
+ import MachineLearning from 'carbon-icons-svelte/lib/MachineLearning.svelte'
4
+ import WarningFilled from 'carbon-icons-svelte/lib/WarningFilled.svelte'
5
+ import WatsonHealthStackedMove from 'carbon-icons-svelte/lib/WatsonHealthStackedMove.svelte'
6
+ import Edit from 'carbon-icons-svelte/lib/Edit.svelte'
7
+ import { enumRequirementStatus, enumRequirementType, PromptIndicators, statusReasonsToFeedback, translateMutations, type PhaseChangeMutations, type StatusReason } from '@reqquest/ui'
8
+ import { isInlineReviewerEditPrompt, isDisplayablePrompt, hasDisplayablePrompts, RenderDisplayComponent, applicantRequirementTypes, reviewerRequirementTypes, api, PromptSaveQueue, type BasicRequestData } from '$internal'
9
+ import { FormInlineNotification, Panel, PanelFormDialog } from '@txstate-mws/carbon-svelte'
10
+ import { Tooltip } from 'carbon-components-svelte'
11
+ import { uiRegistry } from '$local';
12
+ import { Button } from 'carbon-components-svelte'
13
+ import type { PageData } from '../../routes/requests/[id]/approve/[programKey]/$types'
14
+ import { invalidate, invalidateAll } from '$app/navigation'
15
+ import Review from "carbon-icons-svelte/lib/Review.svelte";
16
+ import DelayedSkeleton from '$internal/components/DelayedSkeleton.svelte';
17
+ import WarningIconYellow from './WarningIconYellow.svelte';
18
+ import MissingDefinitionNotification from './MissingDefinitionNotification.svelte';
19
+ import { toasts } from '@txstate-mws/svelte-components';
20
+ import { randomid } from 'txstate-utils'
21
+
22
+ type ApplicationRequirement = (typeof appRequest)['applications'][number]['requirements'][number]
23
+
24
+ export let sections: { key: string, title: string, requirements: ApplicationRequirement[] }[]
25
+ /** the stage panel that gets the "Send to" button once the application is ready for workflow */
26
+ export let lastStageKey: string | undefined = undefined
27
+ export let promptIndicator: Record<string, any>
28
+ export let promptStatusReasons: Record<string, StatusReason[]> = {}
29
+ export let loading = false
30
+ export let appRequest: PageData['appRequest']
31
+ export let application: PageData['appRequest']['applications'][0]
32
+ export let basicRequestData: BasicRequestData
33
+
34
+ type PromptExtraData = Awaited<ReturnType<typeof api.getPromptData>>
35
+ type Prompt = PageData['appRequest']['applications'][0]['requirements'][0]['prompts'][0]
36
+ type PromptWithExtra = Prompt & PromptExtraData & { allowSaveWithoutChanges?: boolean }
37
+ let editingPromptWithData: PromptWithExtra | undefined = undefined
38
+ let promptBeingEdited: Prompt | undefined = undefined
39
+ let showPromptDialog = false
40
+ let fetchingEditPrompt = false
41
+
42
+ // every save on this screen writes the same appRequest row, so they all run in sequence through one
43
+ // queue that threads the row's dataVersion from each save into the next
44
+ let saveQueue = new PromptSaveQueue()
45
+ let saveQueueAppRequestId: string | undefined
46
+ $: {
47
+ if (saveQueueAppRequestId !== appRequest.id) {
48
+ saveQueueAppRequestId = appRequest.id
49
+ saveQueue = new PromptSaveQueue()
50
+ }
51
+ saveQueue.adopt(appRequest.dataVersion)
52
+ }
53
+
54
+ function editPrompt (prompt: Prompt, allowSaveWithoutChanges: boolean = false) {
55
+ return async () => {
56
+ if (fetchingEditPrompt) return
57
+ promptBeingEdited = prompt
58
+ fetchingEditPrompt = true
59
+ showPromptDialog = true
60
+ try {
61
+ // empty the save queue before fetching, so the version we pin to the modal includes everything
62
+ // this session has typed inline.
63
+ await saveQueue.drain()
64
+ const extra = await api.getPromptData(appRequest.id, prompt.id)
65
+ editingPromptWithData = { ...prompt, ...extra, allowSaveWithoutChanges }
66
+ } finally {
67
+ fetchingEditPrompt = false
68
+ }
69
+ }
70
+ }
71
+ // Diff issue, but needed because a bare document.querySelector('.tcbs-dialog') grabs the first
72
+ // dialog in document order, and the page keeps note form and the notes list permanently mounted.
73
+ // so need to be specific
74
+ const editDialogFormId = randomid()
75
+ function editDialogElement () {
76
+ return document.getElementById(editDialogFormId)?.closest('.tcbs-dialog') ?? undefined
77
+ }
78
+ function hideEditModalPrompt () { // keep the dialog in the DOM so on:saved fires, but remove from view
79
+ if (!showPromptDialog) return
80
+ editDialogElement()?.classList.add('invisible')
81
+ }
82
+ function showEditModalPrompt () {
83
+ editDialogElement()?.classList.remove('invisible')
84
+ }
85
+
86
+ function closePromptDialog () {
87
+ fetchingEditPrompt = false
88
+ showPromptDialog = false
89
+ editingPromptWithData = undefined
90
+ promptBeingEdited = undefined
91
+ }
92
+ function onPromptSubmit (prompt: any, modal = false) {
93
+ return async (data: any) => {
94
+ if (modal) { loading = true; hideEditModalPrompt() }
95
+ try {
96
+ // the modal is editing the data it fetched when it opened, so it saves against that version;
97
+ // inline forms carry whatever version the queue is holding when their turn comes up
98
+ const pinnedVersion = modal ? prompt.dataVersion : undefined
99
+ const response = await saveQueue.save(async dataVersion => {
100
+ return (!prompt.allowSaveWithoutChanges)
101
+ ? await api.updatePrompt(prompt.id, data, false, dataVersion)
102
+ : await api.updatePrompt(prompt.id, data, false, dataVersion, true) // triggers from review corrections edit selection, allow saving without changes to handle invalidate prompts that require no changes
103
+ }, pinnedVersion)
104
+ if (modal && !response.success) { loading = false; showEditModalPrompt() }
105
+ // prev resp.data was replacing the store's data with a wrongly-shaped object and
106
+ // re-baselines beforeUserChanges to it. Reduce to this prompt's slice like ApplicantPromptPage.onSubmit
107
+ return { ...response, data: response.data?.[prompt.key] }
108
+ } catch (e) {
109
+ if (modal) { loading = false; showEditModalPrompt() }
110
+ throw e
111
+ }
112
+ }
113
+ }
114
+
115
+ function onPromptValidate (prompt: any) {
116
+ return async (data: any) => {
117
+ const response = await api.updatePrompt(prompt.id, data, true, prompt.dataVersion)
118
+ return response.messages
119
+ }
120
+ }
121
+
122
+ async function refreshReviewData () {
123
+ // Scoped instead of invalidateAll()
124
+ await Promise.all([invalidate('request:approve'), invalidate('request:id')])
125
+ }
126
+
127
+ async function appRequestPhaseChange (action: PhaseChangeMutations) {
128
+ const response = await api.appRequestPhaseChange(appRequest.id, action)
129
+ await invalidateAll()
130
+ loading = false
131
+ if (!response.success) {
132
+ toasts.add({
133
+ type: 'error',
134
+ title: 'Action Failed',
135
+ message: response.messages.map(m => m.message).join('\n') || 'An unknown error occurred.'
136
+ })
137
+ } else {
138
+ toasts.add({
139
+ type: 'success',
140
+ message: `Successfully ${translateMutations[action]}.`
141
+ })
142
+ }
143
+ await invalidateAll()
144
+ }
145
+
146
+ async function advanceWorkflow () {
147
+ loading = true
148
+ const response = await api.advanceWorkflow(application.id)
149
+ await invalidateAll()
150
+ if (basicRequestData?.actions.completeReview) return await appRequestPhaseChange('completeReview')
151
+ if (basicRequestData?.actions.completeRequest) return await appRequestPhaseChange('completeRequest')
152
+ loading = false
153
+ if (!response.success) {
154
+ toasts.add({
155
+ type: 'error',
156
+ title: 'Could not advance application',
157
+ message: response.messages.map(m => m.message).join('\n') || 'An unknown error occurred.'
158
+ })
159
+ } else {
160
+ toasts.add({
161
+ type: 'success',
162
+ message: 'Application advanced.'
163
+ })
164
+ }
165
+ }
166
+
167
+ async function reverseWorkflow () {
168
+ loading = true
169
+ const response = await api.reverseWorkflow(application.id)
170
+ await invalidateAll()
171
+ loading = false
172
+ if (!response.success) {
173
+ toasts.add({
174
+ type: 'error',
175
+ title: 'Could not reverse application workflow',
176
+ message: response.messages.map(m => m.message).join('\n') || 'An unknown error occurred.'
177
+ })
178
+ } else {
179
+ toasts.add({
180
+ type: 'success',
181
+ message: 'Application workflow reversed.'
182
+ })
183
+ }
184
+ }
185
+
186
+ $: readyForWorkflow = !application.nextWorkflowStage && application.phase === 'READY_FOR_WORKFLOW' ? lastStageKey : undefined
187
+ // otherwise the button sits on the last non-stage panel on the page, wherever the layout put it
188
+ $: latestWorkflow = readyForWorkflow ?? sections.filter(section => section.requirements.every(r => !r.workflowStage?.key)).pop()?.key
189
+
190
+ /** inline autoSave forms: refresh statuses/indicators only, never touch the live form */
191
+ async function onPromptAutoSaved () {
192
+ await refreshReviewData()
193
+ }
194
+
195
+ /** modal edit form: close first, so the dialog is not left hidden for the whole refresh */
196
+ async function onPromptSaved () {
197
+ loading = false
198
+ closePromptDialog()
199
+ await refreshReviewData()
200
+ }
201
+
202
+ </script>
203
+ {#each sections as section (section.key)}
204
+ <Panel title={section.title} expandable expanded>
205
+ {#if section.requirements.some(hasDisplayablePrompts)}
206
+ <dl class="prompts">
207
+ {#each section.requirements as requirement (requirement.id)}
208
+ {#each requirement.prompts.filter(isDisplayablePrompt) as prompt (prompt.id)}
209
+ {@const def = uiRegistry.getPrompt(prompt.key)}
210
+ {@const isReviewerQuestion = reviewerRequirementTypes.has(requirement.type) && !def?.automation}
211
+ {@const isAutomation = !!def?.automation}
212
+ {@const editMode = isInlineReviewerEditPrompt(def, requirement.type, prompt)}
213
+ {@const small = editMode && def?.formMode !== 'full' ? def?.formMode !== 'large' : def?.displayMode !== 'large'}
214
+ {@const large = editMode && def?.formMode !== 'full' ? def?.formMode === 'large' : def?.displayMode === 'large'}
215
+ {@const dtid = `dt-title-${prompt.id}`}
216
+ {@const currentWorkflow = application.workflowStage ? application.workflowStage?.key === section.requirements[0]?.workflowStage?.key : undefined}
217
+ {@const disabled = ((!editMode && def?.formMode !== 'full') && (currentWorkflow || requirement.status !== enumRequirementStatus.MET))}
218
+ {#snippet editButtons()}
219
+ {#if prompt.actions.update}
220
+ {#if prompt.invalidated && !applicantRequirementTypes.has(requirement.type)}
221
+ <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)} />
222
+ {:else}
223
+ <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
+ {/if}
225
+ {/if}
226
+ {/snippet}
227
+ <dt class:small class:large class:isReviewerQuestion class:disabled={disabled} class:bg-tagyellow-200={isAutomation}>
228
+ {#if promptIndicator[prompt.key]?.indicator}
229
+ <div class="indicator-tooltip">
230
+ <Tooltip align="start" direction="bottom">
231
+ <svelte:fragment slot="icon">
232
+ {#if promptIndicator[prompt.key]?.indicator === PromptIndicators.AUTOMATION}
233
+ <MachineLearning size={20} />
234
+ {:else if promptIndicator[prompt.key]?.indicator === PromptIndicators.WARNING}
235
+ <WarningIconYellow />
236
+ {:else if promptIndicator[prompt.key]?.indicator === PromptIndicators.DISQUALIFYING}
237
+ <WarningFilled size={20} class="disqualifying-icon" style="fill: var(--cds-support-01, #da1e28)" />
238
+ {:else if promptIndicator[prompt.key]?.indicator === PromptIndicators.PENDING}
239
+ <WatsonHealthStackedMove size={20} class="pending-icon" style="fill: var(--cds-support-04, #0043ce)" />
240
+ {/if}
241
+ </svelte:fragment>
242
+ {promptIndicator[prompt.key]?.reason}
243
+ </Tooltip>
244
+ </div>
245
+ {/if}
246
+ <div id={dtid}>
247
+ {prompt.title}
248
+ </div>
249
+ {#if large && !editMode}
250
+ {@render editButtons()}
251
+ {/if}
252
+ </dt>
253
+ <dd class="flow" class:small class:large class:isReviewerQuestion class:disabled={disabled} class:bg-tagyellow-200={isAutomation} role={editMode ? 'group' : undefined} aria-labelledby={dtid}>
254
+ {#if editMode}
255
+ <Form bind:store={saveQueue.stores[prompt.id]} preload={prompt.preloadData} submit={onPromptSubmit(prompt)} validate={onPromptValidate(prompt)} autoSave on:autosaved={onPromptAutoSaved} let:data let:messages>
256
+ <svelte:component this={def?.formComponent} {data} appRequestData={appRequest.data} prestageData={{latest: prompt.prestageData, current: appRequest.data[prompt.key]?.__prestage}} fetched={prompt.fetchedData} configData={prompt.configurationData} gatheredConfigData={prompt.gatheredConfigData} invalidated={prompt.invalidated} invalidatedReason={prompt.invalidatedReason} statusReasons={promptStatusReasons[prompt.key] ?? []} />
257
+ {#each statusReasonsToFeedback(promptStatusReasons[prompt.key], { blamedOnly: true, statuses: uiRegistry.statusReasonNotificationStatuses(prompt.key, 'reviewer') }) as message}
258
+ <FormInlineNotification {message} />
259
+ {/each}
260
+ {#each messages as message (message.message, message.type)}
261
+ <FormInlineNotification {message} />
262
+ {/each}
263
+ </Form>
264
+ {:else}
265
+ <div class="pr-4">
266
+ <RenderDisplayComponent
267
+ {def}
268
+ appRequestId={appRequest.id}
269
+ appData={appRequest.data}
270
+ prompt={prompt}
271
+ prestageData={{latest: prompt.prestageData, current: appRequest.data[prompt.key]?.__prestage}}
272
+ configData={prompt.configurationData}
273
+ gatheredConfigData={prompt.gatheredConfigData} showMoot
274
+ statusReasons={promptStatusReasons[prompt.key] ?? []}
275
+ showInlineReviewNotification={prompt.actions.update && prompt.invalidated && !applicantRequirementTypes.has(requirement.type)} />
276
+ </div>
277
+ {#if !large}
278
+ {@render editButtons()}
279
+ {/if}
280
+ {/if}
281
+ </dd>
282
+ {/each}
283
+ {/each}
284
+ </dl>
285
+ {:else if section.requirements[0]?.workflowStage && section.requirements[0]?.workflowStage.key === application.workflowStage?.key}
286
+ No questions need to be answered in this section. You may advance to the next step.
287
+ {:else}
288
+ No questions in this section.
289
+ {/if}
290
+ {#if !section.requirements.every(r => r.type === enumRequirementType.PREQUAL) && (application.actions?.advanceWorkflow || application.actions?.reverseWorkflow)}
291
+ <div class="flex justify-end mt-8">
292
+ {#if application.actions?.advanceWorkflow && (application.workflowStage?.key ? application.workflowStage?.key === section.requirements[0]?.workflowStage?.key : section.key === latestWorkflow)}
293
+ <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}
295
+ <Button kind='secondary' size="small" on:click={reverseWorkflow}>Edit answers</Button>
296
+ {/if}
297
+ </div>
298
+ {/if}
299
+ </Panel>
300
+ {/each}
301
+
302
+ {#if showPromptDialog && promptBeingEdited}
303
+ {@const formMode = uiRegistry.getPrompt(promptBeingEdited.key)?.formMode === 'full' ? 'large' : undefined}
304
+ <PanelFormDialog
305
+ id={editDialogFormId}
306
+ title={editingPromptWithData?.invalidated ? `Review correction "${editingPromptWithData?.title}"` : 'Edit Prompt'}
307
+ bind:open={showPromptDialog}
308
+ on:cancel={closePromptDialog}
309
+ submit={onPromptSubmit(editingPromptWithData, true)}
310
+ validate={onPromptValidate(editingPromptWithData)}
311
+ on:saved={onPromptSaved}
312
+ disableSaveUntilChanged={!editingPromptWithData?.allowSaveWithoutChanges} // allow saving without changes if prompt was previously invalidated ...accomodates reviewer saying no changes required on correction check
313
+ centered={!formMode}
314
+ size={formMode}
315
+ preload={editingPromptWithData?.preloadData}
316
+ let:data
317
+ >
318
+ {#if editingPromptWithData}
319
+ {@const def = uiRegistry.getPrompt(editingPromptWithData.key)}
320
+ <div class='font-medium text-center mt-2'>
321
+ <p class="text-xl font-medium ">{editingPromptWithData.title}</p>
322
+ </div>
323
+ {#if def?.formComponent == null}
324
+ <MissingDefinitionNotification kind="prompt" definitionKey={editingPromptWithData.key} />
325
+ {:else}
326
+ <svelte:component
327
+ this={def.formComponent}
328
+ appRequestId={appRequest.id}
329
+ {data}
330
+ appRequestData={editingPromptWithData.data}
331
+ prestageData={{
332
+ latest: editingPromptWithData.prestageData,
333
+ current: appRequest.data[editingPromptWithData.key]?.__prestage
334
+ }}
335
+ fetched={editingPromptWithData.fetchedData}
336
+ configData={editingPromptWithData.configurationData}
337
+ gatheredConfigData={editingPromptWithData.gatheredConfigData}
338
+ invalidated={editingPromptWithData.invalidated}
339
+ invalidatedReason={editingPromptWithData.invalidatedReason}
340
+ statusReasons={promptStatusReasons[editingPromptWithData.key] ?? []}
341
+ />
342
+ {#each statusReasonsToFeedback(promptStatusReasons[editingPromptWithData.key], { blamedOnly: true, statuses: uiRegistry.statusReasonNotificationStatuses(editingPromptWithData.key, 'reviewer') }) as message}
343
+ <FormInlineNotification {message} />
344
+ {/each}
345
+ {/if}
346
+ {:else if fetchingEditPrompt}
347
+ {@const loader = uiRegistry.getPrompt(promptBeingEdited.key)?.loader}
348
+ <div class='font-medium text-center mt-2'>
349
+ <p class="text-xl font-medium ">{promptBeingEdited.title}</p>
350
+ </div>
351
+ {#if loader}
352
+ <DelayedSkeleton {loader} />
353
+ {/if}
354
+ {/if}
355
+ </PanelFormDialog>
356
+ {/if}
357
+
358
+ <style>
359
+ dl.prompts {
360
+ display: grid;
361
+ grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
362
+ align-items: stretch;
363
+ gap: 0;
364
+ margin: -16px;
365
+ }
366
+ .prompts dt, .prompts dd {
367
+ position: relative;
368
+ border-bottom: 1px solid var(--cds-border-subtle);
369
+ padding: 1rem 15px;
370
+ }
371
+ .prompts dt.small {
372
+ padding-right: 15px;
373
+ }
374
+ .prompts dt.large, .prompts dd.large {
375
+ grid-column: span 2;
376
+ }
377
+ /* When a display component's root is a PromptDisplayGrid — directly, or through the
378
+ <form> of an inline edit (formMode 'large') — hand it our column tracks via subgrid
379
+ so its challenge/response rows align with the real prompt rows. */
380
+ .prompts dd.large:has(> :global(.prompt-display-grid)),
381
+ .prompts dd.large:has(> :global(form > .prompt-display-grid)) {
382
+ display: grid;
383
+ grid-template-columns: subgrid;
384
+ align-content: start;
385
+ padding: 0;
386
+ border-bottom: none;
387
+ --prompt-display-subgrid: subgrid;
388
+ --prompt-display-row-padding: 1rem 15px;
389
+ --prompt-display-challenge-padding-left: 32px;
390
+ }
391
+ .prompts dd.large:has(> :global(.prompt-display-grid)) > :global(*),
392
+ .prompts dd.large:has(> :global(form > .prompt-display-grid)) > :global(*) {
393
+ grid-column: 1 / -1;
394
+ }
395
+ .prompts dd.large > :global(form:has(> .prompt-display-grid)) {
396
+ display: grid;
397
+ grid-template-columns: subgrid;
398
+ }
399
+ .prompts dd.large > :global(form:has(> .prompt-display-grid) > *) {
400
+ grid-column: 1 / -1;
401
+ }
402
+ .prompts dt.isReviewerQuestion, .prompts dd.isReviewerQuestion {
403
+ background-color: var(--background-cool-light);
404
+ }
405
+ .prompts dt.isReviewerQuestion.disabled, .prompts dd.isReviewerQuestion.disabled {
406
+ background-color: #f2f2f2;
407
+ color: #8C8C8C;
408
+ }
409
+ .prompts dt {
410
+ display: flex;
411
+ align-items: center;
412
+ gap: 4px;
413
+ padding-left: 32px;
414
+ }
415
+ .prompts dt :global(.bx--btn.prompt-edit),
416
+ .prompts dd :global(.bx--btn.prompt-edit) {
417
+ position: absolute;
418
+ top: 0.35rem;
419
+ right: 0;
420
+ margin: 0;
421
+ }
422
+ .prompts dt .indicator-tooltip {
423
+ display: inline-block;
424
+ margin-left: -24px;
425
+ }
426
+ .prompts dt .indicator-tooltip :global(> div) {
427
+ line-height: 0;
428
+ }
429
+ .prompts dt .indicator-tooltip :global(> div .bx--tooltip--shown) {
430
+ line-height: 1.2;
431
+ }
432
+ .prompts dt :global(.warning-icon) {
433
+ fill: var(--cds-inverse-support-03, #f1c21b);
434
+ }
435
+ :global(.disqualifying-icon) {
436
+ fill: var(--cds-support-01, #da1e28);
437
+ }
438
+ :global(.pending-icon) {
439
+ fill: var(--cds-support-04, #0043ce);
440
+ }
441
+ :global(.panel:has(.isReviewerQuestion.disabled) .panel-header) {
442
+ background-color: #8C8C8C !important;
443
+ }
444
+ </style>
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import { BadgeNumber } from '@txstate-mws/carbon-svelte'
3
+ import { Accordion, AccordionItem } from 'carbon-components-svelte'
4
+ import { WarningFilled } from 'carbon-icons-svelte'
5
+ import WarningIconYellow from './WarningIconYellow.svelte'
6
+
7
+ export let items: { id: string, message: string }[] = []
8
+ export let variant: 'warning' | 'error' = 'error'
9
+ export let accordionTitle = 'Multiple items'
10
+ export let gap = 2
11
+ export let icon = false
12
+
13
+ $: badgeStyle = variant === 'warning'
14
+ ? '--badge-bg: var(--yellow-01, #F3D690); --badge-text: #6F510C'
15
+ : '--badge-bg: #FBE9EA; --badge-text: #a11c25'
16
+ </script>
17
+
18
+ {#if items.length === 1}
19
+ <div class="flex items-center gap-{gap}">
20
+ {#if icon}
21
+ {#if variant === 'warning'}
22
+ <WarningIconYellow />
23
+ {:else}
24
+ <WarningFilled size={20} class="disqualifying-icon" />
25
+ {/if}
26
+ {:else}
27
+ <BadgeNumber value={1} class="ml-0" style={badgeStyle} />
28
+ {/if}
29
+ <p class="text-sm">{items[0].message}</p>
30
+ </div>
31
+ {:else if items.length > 1}
32
+ <div class="flex">
33
+ {#if icon}
34
+ {#if variant === 'warning'}
35
+ <WarningIconYellow class="mt-5 mr-2" />
36
+ {:else}
37
+ <WarningFilled size={20} class="disqualifying-icon" />
38
+ {/if}
39
+ {:else}
40
+ <BadgeNumber value={items.length} class="mt-5 mr-2 ml-0" style={badgeStyle} />
41
+ {/if}
42
+ <div class="mt-2 w-full">
43
+ <Accordion align="start">
44
+ <AccordionItem title={accordionTitle}>
45
+ <ol class="list-decimal">
46
+ {#each items as item (item.id)}
47
+ <li>{item.message}</li>
48
+ {/each}
49
+ </ol>
50
+ </AccordionItem>
51
+ </Accordion>
52
+ </div>
53
+ </div>
54
+ {/if}