@reqquest/ui 1.0.0 → 1.1.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.
- package/README.md +8 -0
- package/dist/api.js +656 -111
- package/dist/components/AppRequestCard.svelte +172 -0
- package/dist/components/ApplicantProgramList.svelte +184 -0
- package/dist/components/ApplicantProgramListTooltip.svelte +22 -0
- package/dist/components/ApplicantPromptPage.svelte +88 -0
- package/dist/components/ApplicationDetailsView.svelte +307 -0
- package/dist/components/ButtonLoadingIcon.svelte +2 -1
- package/dist/components/FieldCardCheckbox.svelte +328 -0
- package/dist/components/FieldCardRadio.svelte +320 -0
- package/dist/components/IntroPanel.svelte +41 -0
- package/dist/components/PeriodPanel.svelte +100 -0
- package/dist/components/QuestionnairePrompt.svelte +36 -0
- package/dist/components/RenderDisplayComponent.svelte +38 -0
- package/dist/components/ReviewerList.svelte +93 -0
- package/dist/components/StatusMessageList.svelte +35 -0
- package/dist/components/WarningIconYellow.svelte +20 -0
- package/dist/components/index.js +11 -0
- package/dist/components/types.js +1 -0
- package/dist/csv.js +21 -0
- package/dist/index.js +2 -0
- package/dist/status-utils.js +343 -0
- package/dist/stores/IStateStore.js +0 -1
- package/dist/typed-client/schema.graphql +564 -124
- package/dist/typed-client/schema.js +87 -23
- package/dist/typed-client/types.js +919 -454
- package/dist/util.js +12 -1
- package/package.json +39 -40
- package/dist/api.d.ts +0 -595
- package/dist/components/ButtonLoadingIcon.svelte.d.ts +0 -18
- package/dist/components/index.d.ts +0 -1
- package/dist/index.d.ts +0 -4
- package/dist/registry.d.ts +0 -138
- package/dist/stores/IStateStore.d.ts +0 -5
- package/dist/typed-client/index.d.ts +0 -25
- package/dist/typed-client/runtime/batcher.d.ts +0 -105
- package/dist/typed-client/runtime/createClient.d.ts +0 -17
- package/dist/typed-client/runtime/error.d.ts +0 -18
- package/dist/typed-client/runtime/fetcher.d.ts +0 -10
- package/dist/typed-client/runtime/generateGraphqlOperation.d.ts +0 -30
- package/dist/typed-client/runtime/index.d.ts +0 -11
- package/dist/typed-client/runtime/linkTypeMap.d.ts +0 -9
- package/dist/typed-client/runtime/typeSelection.d.ts +0 -28
- package/dist/typed-client/runtime/types.d.ts +0 -55
- package/dist/typed-client/schema.d.ts +0 -1483
- package/dist/typed-client/types.d.ts +0 -540
- package/dist/util.d.ts +0 -2
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
import type { CardImage, TagItem } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
|
|
4
|
+
export interface CardSelectItem {
|
|
5
|
+
value: any
|
|
6
|
+
title: string
|
|
7
|
+
subhead?: string
|
|
8
|
+
image?: CardImage
|
|
9
|
+
tags?: TagItem[]
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
selectionLabel?: string
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { FORM_CONTEXT, FORM_INHERITED_PATH, Field, type FormStore } from '@txstate-mws/svelte-forms'
|
|
17
|
+
import { Store } from '@txstate-mws/svelte-store'
|
|
18
|
+
import { createEventDispatcher, getContext } from 'svelte'
|
|
19
|
+
import { get, isNotBlank, equal } from 'txstate-utils'
|
|
20
|
+
import { Card, CardGrid, FormInlineNotification } from '@txstate-mws/carbon-svelte'
|
|
21
|
+
|
|
22
|
+
const dispatch = createEventDispatcher()
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Path to the form field in the data structure
|
|
26
|
+
* @type {string}
|
|
27
|
+
*/
|
|
28
|
+
export let path: string
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Array of card options
|
|
32
|
+
* @type {CardSelectItem[]}
|
|
33
|
+
*/
|
|
34
|
+
export let items: CardSelectItem[]
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set to true to treat the value as a number instead of string
|
|
38
|
+
* @type {boolean}
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
export let number = false
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set to true to treat the value as a boolean
|
|
45
|
+
* @type {boolean}
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
export let boolean = false
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set to true to prevent null values
|
|
52
|
+
* @type {boolean}
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
export let notNull = false
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether the field group is conditional
|
|
59
|
+
* @type {boolean}
|
|
60
|
+
* @default true
|
|
61
|
+
*/
|
|
62
|
+
export let conditional = true
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Default selected value
|
|
66
|
+
* @type {any}
|
|
67
|
+
* @default undefined
|
|
68
|
+
*/
|
|
69
|
+
export let defaultValue: any = undefined
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Text to display in the fieldset legend
|
|
73
|
+
* @type {string | undefined}
|
|
74
|
+
* @default undefined
|
|
75
|
+
*/
|
|
76
|
+
export let legendText: string | undefined = undefined
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* When true, the field is marked as required in the UI. This is only for visual indication
|
|
80
|
+
* and does not affect validation. Validation is handled by the API that receives the data.
|
|
81
|
+
* @type {boolean}
|
|
82
|
+
* @default false
|
|
83
|
+
*/
|
|
84
|
+
export let required = false
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* CSS size for the grid minimum item width (like CardGrid)
|
|
88
|
+
* @type {string}
|
|
89
|
+
* @default '20rem'
|
|
90
|
+
*/
|
|
91
|
+
export let cardSize = '20rem'
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Gap between cards in the grid
|
|
95
|
+
* @type {string}
|
|
96
|
+
* @default '16px'
|
|
97
|
+
*/
|
|
98
|
+
export let gap = '16px'
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Custom function to serialize values for form submission
|
|
102
|
+
* @type {((value: any) => string) | undefined}
|
|
103
|
+
* @default undefined
|
|
104
|
+
*/
|
|
105
|
+
export let serialize: ((value: any) => string) | undefined = undefined
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Custom function to deserialize values from form data
|
|
109
|
+
* @type {((value: string) => any) | undefined}
|
|
110
|
+
* @default undefined
|
|
111
|
+
*/
|
|
112
|
+
export let deserialize: ((value: string) => any) | undefined = undefined
|
|
113
|
+
|
|
114
|
+
const store = getContext<FormStore>(FORM_CONTEXT)
|
|
115
|
+
const inheritedPath = getContext<string>(FORM_INHERITED_PATH)
|
|
116
|
+
const finalPath = [inheritedPath, path].filter(isNotBlank).join('.')
|
|
117
|
+
|
|
118
|
+
const itemStore = new Store(items)
|
|
119
|
+
$: itemStore.set(items)
|
|
120
|
+
|
|
121
|
+
let finalSerialize: ((value: any) => string) | undefined
|
|
122
|
+
let finalDeserialize: ((value: string) => any) | undefined
|
|
123
|
+
|
|
124
|
+
function onSelect (item: CardSelectItem) {
|
|
125
|
+
return () => {
|
|
126
|
+
if (item.disabled) return
|
|
127
|
+
const deserialized = item.value
|
|
128
|
+
void store.setField(finalPath, deserialized)
|
|
129
|
+
dispatch('update', deserialized)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function reactToItems (..._: any[]) {
|
|
134
|
+
if (!finalDeserialize) return
|
|
135
|
+
if (!items.length) {
|
|
136
|
+
return await store.setField(finalPath, finalDeserialize(''), { notDirty: true })
|
|
137
|
+
}
|
|
138
|
+
const val = get($store.data, finalPath)
|
|
139
|
+
if (!items.some(o => equal(o.value, val))) {
|
|
140
|
+
await store.setField(finalPath, notNull && items.some(o => equal(o.value, defaultValue)) ? defaultValue : finalDeserialize(''), { notDirty: true })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
$: reactToItems($itemStore, finalDeserialize).catch(console.error)
|
|
144
|
+
|
|
145
|
+
// Keyboard navigation
|
|
146
|
+
let activeIdx = 0
|
|
147
|
+
const cardElements: HTMLDivElement[] = []
|
|
148
|
+
|
|
149
|
+
function findNextEnabled (startIdx: number, direction: 1 | -1): number {
|
|
150
|
+
let idx = startIdx
|
|
151
|
+
let attempts = 0
|
|
152
|
+
while (attempts < items.length) {
|
|
153
|
+
idx = (idx + direction + items.length) % items.length
|
|
154
|
+
if (!items[idx]?.disabled) return idx
|
|
155
|
+
attempts++
|
|
156
|
+
}
|
|
157
|
+
return startIdx // All disabled, stay put
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function focusCard (idx: number) {
|
|
161
|
+
if (idx >= 0 && idx < items.length && !items[idx]?.disabled) {
|
|
162
|
+
activeIdx = idx
|
|
163
|
+
cardElements[idx]?.focus()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleKeyDown (e: KeyboardEvent, idx: number) {
|
|
168
|
+
switch (e.key) {
|
|
169
|
+
case 'ArrowRight':
|
|
170
|
+
case 'ArrowDown':
|
|
171
|
+
e.preventDefault()
|
|
172
|
+
focusCard(findNextEnabled(idx, 1))
|
|
173
|
+
break
|
|
174
|
+
case 'ArrowLeft':
|
|
175
|
+
case 'ArrowUp':
|
|
176
|
+
e.preventDefault()
|
|
177
|
+
focusCard(findNextEnabled(idx, -1))
|
|
178
|
+
break
|
|
179
|
+
case ' ':
|
|
180
|
+
case 'Enter':
|
|
181
|
+
e.preventDefault()
|
|
182
|
+
if (!items[idx]?.disabled) {
|
|
183
|
+
onSelect(items[idx])()
|
|
184
|
+
}
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
</script>
|
|
189
|
+
<!--
|
|
190
|
+
@component
|
|
191
|
+
|
|
192
|
+
A form field component that displays a grid of selectable cards (single selection).
|
|
193
|
+
Similar to FieldRadioTile but with richer card-based visuals including images and tags.
|
|
194
|
+
|
|
195
|
+
By default this component expects values to be strings. If you wish to use other types of values, you
|
|
196
|
+
must provide a `serialize` and `deserialize` function. If you set the `number` prop, an appropriate
|
|
197
|
+
serializer and deserializer will be provided by default.
|
|
198
|
+
|
|
199
|
+
If your `items` are not ready on first load (e.g. they're being loaded from an API fetch), you must
|
|
200
|
+
place this field inside an `{#if}` block until they are ready.
|
|
201
|
+
-->
|
|
202
|
+
<Field {path} {notNull} {conditional} {defaultValue} {boolean} {number} {serialize} {deserialize} bind:finalSerialize bind:finalDeserialize let:messages let:value let:invalid let:onBlur>
|
|
203
|
+
<div on:focusin={() => dispatch('focus')} on:focusout={() => { onBlur(); dispatch('blur') }}>
|
|
204
|
+
<fieldset class="card-select-fieldset">
|
|
205
|
+
{#if legendText}
|
|
206
|
+
<legend class="bx--label">{legendText}{#if required} <span aria-hidden="true"> *</span>{/if}</legend>
|
|
207
|
+
{/if}
|
|
208
|
+
<CardGrid {cardSize} {gap}>
|
|
209
|
+
{#each $itemStore as item, index (item.value)}
|
|
210
|
+
{@const selected = finalSerialize ? value === finalSerialize(item.value) : equal(value, item.value)}
|
|
211
|
+
<div
|
|
212
|
+
bind:this={cardElements[index]}
|
|
213
|
+
class="selectable-card"
|
|
214
|
+
class:selected
|
|
215
|
+
class:disabled={item.disabled}
|
|
216
|
+
role="radio"
|
|
217
|
+
aria-checked={selected}
|
|
218
|
+
aria-disabled={item.disabled}
|
|
219
|
+
tabindex={item.disabled ? -1 : (index === activeIdx ? 0 : -1)}
|
|
220
|
+
on:click={onSelect(item)}
|
|
221
|
+
on:keydown={e => handleKeyDown(e, index)}
|
|
222
|
+
>
|
|
223
|
+
<Card
|
|
224
|
+
title={item.title}
|
|
225
|
+
subhead={item.subhead}
|
|
226
|
+
image={item.image}
|
|
227
|
+
tags={item.tags ?? []}
|
|
228
|
+
actions={[]}
|
|
229
|
+
navigations={[]}
|
|
230
|
+
>
|
|
231
|
+
<slot {item} {selected} {index} />
|
|
232
|
+
</Card>
|
|
233
|
+
<div class="selection-indicator justify-center">
|
|
234
|
+
<span class="radio-circle" class:checked={selected}></span>
|
|
235
|
+
<span class="selection-label">{item.selectionLabel ?? 'Choose this option'}</span>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
{/each}
|
|
239
|
+
</CardGrid>
|
|
240
|
+
</fieldset>
|
|
241
|
+
</div>
|
|
242
|
+
{#each messages as message (message)}
|
|
243
|
+
<FormInlineNotification {message} />
|
|
244
|
+
{/each}
|
|
245
|
+
</Field>
|
|
246
|
+
|
|
247
|
+
<style>
|
|
248
|
+
.card-select-fieldset {
|
|
249
|
+
border: none;
|
|
250
|
+
padding: 0;
|
|
251
|
+
margin: 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.selectable-card {
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
border: 2px solid transparent;
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
outline: none;
|
|
259
|
+
transition: border-color 0.15s ease;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.selectable-card:focus-visible {
|
|
263
|
+
outline: 2px solid var(--cds-focus, #0f62fe);
|
|
264
|
+
outline-offset: 2px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.selectable-card:hover:not(.disabled) {
|
|
268
|
+
border-color: var(--cds-interactive-03, #0353e9);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.selectable-card.selected {
|
|
272
|
+
border-color: var(--cds-interactive-01, #0f62fe);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.selectable-card.disabled {
|
|
276
|
+
cursor: not-allowed;
|
|
277
|
+
opacity: 0.5;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.selection-indicator {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: 0.5rem;
|
|
284
|
+
padding: 0.75rem 1rem;
|
|
285
|
+
background-color: var(--cds-ui-01, #f4f4f4);
|
|
286
|
+
border-top: 1px solid var(--cds-ui-03, #e0e0e0);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.radio-circle {
|
|
290
|
+
width: 1.125rem;
|
|
291
|
+
height: 1.125rem;
|
|
292
|
+
border: 2px solid var(--cds-icon-01, #161616);
|
|
293
|
+
border-radius: 50%;
|
|
294
|
+
flex-shrink: 0;
|
|
295
|
+
position: relative;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.radio-circle.checked::after {
|
|
299
|
+
content: '';
|
|
300
|
+
position: absolute;
|
|
301
|
+
top: 50%;
|
|
302
|
+
left: 50%;
|
|
303
|
+
transform: translate(-50%, -50%);
|
|
304
|
+
width: 0.5rem;
|
|
305
|
+
height: 0.5rem;
|
|
306
|
+
background-color: var(--cds-interactive-01, #0f62fe);
|
|
307
|
+
border-radius: 50%;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.selection-label {
|
|
311
|
+
font-size: 0.875rem;
|
|
312
|
+
color: var(--cds-text-01, #161616);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Remove Card's default border when inside selectable-card */
|
|
316
|
+
.selectable-card :global(.card) {
|
|
317
|
+
border: none;
|
|
318
|
+
box-shadow: none;
|
|
319
|
+
}
|
|
320
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TagSet } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
import type { TagItem } from '@txstate-mws/carbon-svelte'
|
|
4
|
+
|
|
5
|
+
export let title = ''
|
|
6
|
+
export let subtitle = ''
|
|
7
|
+
export let tags: TagItem[] | undefined = undefined
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<section class="bg-[var(--cds-ui-01)] py-4 px-[16px] text-2xl mb-2">
|
|
11
|
+
<div class="repel">
|
|
12
|
+
<div class="max-w-lg flex flex-col flex-wrap justify-start">
|
|
13
|
+
{#if title}
|
|
14
|
+
<div class="title-block [ flex gap-[12px] ] ">
|
|
15
|
+
<h2 class="text-[1.25rem] text-[var(--cds-text-01,#1A1A1A)] font-normal leading-7">
|
|
16
|
+
{title}
|
|
17
|
+
</h2>
|
|
18
|
+
{#if tags}
|
|
19
|
+
<TagSet tags={tags} />
|
|
20
|
+
{/if}
|
|
21
|
+
</div>
|
|
22
|
+
{#if subtitle}
|
|
23
|
+
<p class="text-sm text-[var(--cds-text-02)] leading-[18px] tracking-[0.16px] pt-2">
|
|
24
|
+
{subtitle}
|
|
25
|
+
</p>
|
|
26
|
+
{/if}
|
|
27
|
+
{/if}
|
|
28
|
+
<!-- Optional content slot below title/subtitle -->
|
|
29
|
+
{#if $$slots.default}
|
|
30
|
+
<div class="mt-2">
|
|
31
|
+
<slot />
|
|
32
|
+
</div>
|
|
33
|
+
{/if}
|
|
34
|
+
</div>
|
|
35
|
+
{#if $$slots['block-end']}
|
|
36
|
+
<div class="block-end-slot">
|
|
37
|
+
<slot name="block-end" />
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ActionSet, Panel, TagSet } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
import Tab from "carbon-components-svelte/src/Tabs/Tab.svelte";
|
|
4
|
+
import TabContent from "carbon-components-svelte/src/Tabs/TabContent.svelte";
|
|
5
|
+
import Tabs from "carbon-components-svelte/src/Tabs/Tabs.svelte";
|
|
6
|
+
import Tag from "carbon-components-svelte/src/Tag/Tag.svelte";
|
|
7
|
+
import SettingsEdit from "carbon-icons-svelte/lib/SettingsEdit.svelte";
|
|
8
|
+
import View from "carbon-icons-svelte/lib/View.svelte";
|
|
9
|
+
import { invalidate } from '$app/navigation'
|
|
10
|
+
import { api } from '../api'
|
|
11
|
+
import { page } from '$app/stores'
|
|
12
|
+
import type { UIRegistry } from '../registry'
|
|
13
|
+
import { groupby, pluralize } from 'txstate-utils'
|
|
14
|
+
|
|
15
|
+
export let program: any
|
|
16
|
+
export let sharedProgramRequirements: any
|
|
17
|
+
export let openModal: any
|
|
18
|
+
export let onClick: any
|
|
19
|
+
export let uiRegistry: UIRegistry
|
|
20
|
+
|
|
21
|
+
const disablePeriodProgram = (requirementKey: string) => async () => {
|
|
22
|
+
const res = await api.disablePeriodProgramRequirements($page.params.id!, requirementKey, true)
|
|
23
|
+
await invalidate('api:getPeriodConfigurations')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const enablePeriodProgram = (requirementKey: string) => async () => {
|
|
27
|
+
const res = await api.disablePeriodProgramRequirements($page.params.id!, requirementKey, false)
|
|
28
|
+
await invalidate('api:getPeriodConfigurations')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
$: enabledRequirements = Object.entries(groupby(program.requirements.filter(r => r.enabled), 'type'))
|
|
32
|
+
$: disabledRequirements = program.requirements.filter(r => !r.enabled)
|
|
33
|
+
|
|
34
|
+
</script>
|
|
35
|
+
<Panel title={program.title} expandable expanded>
|
|
36
|
+
{#each enabledRequirements as requrementEntries}
|
|
37
|
+
{@const type = requrementEntries[0]}
|
|
38
|
+
{@const requirements = requrementEntries[1]}
|
|
39
|
+
<Panel title='' expandable expanded>
|
|
40
|
+
<div style="display: content" slot="headerLeft">
|
|
41
|
+
<TagSet tags={[{ label: `Applicant: ${type}`, type: 'purple' }]} />
|
|
42
|
+
</div>
|
|
43
|
+
<div style="display: content" slot="headerRight">
|
|
44
|
+
<TagSet tags={[{ label: `${requirements.length} ${pluralize('requirement', requirements.length)}`, type: 'yellow' }]} />
|
|
45
|
+
</div>
|
|
46
|
+
<Tabs autoWidth>
|
|
47
|
+
<Tab label={`Enabled Requirements (${enabledRequirements.length})`} />
|
|
48
|
+
<Tab label={`Disabled Requirements (${disabledRequirements.length})`} />
|
|
49
|
+
<svelte:fragment slot='content'>
|
|
50
|
+
<TabContent>
|
|
51
|
+
{#each requirements as requirement (requirement.key)}
|
|
52
|
+
{@const reqDef = uiRegistry.getRequirement(requirement.key)}
|
|
53
|
+
<Panel title={requirement.title} expandable noPrimaryAction actions={[{ label: 'Configure requirement', onClick: onClick('requirement', requirement), disabled: reqDef?.configureComponent == null || !requirement.configuration.actions.update }, { label: 'Disable Requirement', onClick: disablePeriodProgram(requirement.key) }]}>
|
|
54
|
+
<div style="display: content" slot="headerLeft">
|
|
55
|
+
<TagSet tags={[{ label: 'Requirement', type: 'yellow' }]} />
|
|
56
|
+
</div>
|
|
57
|
+
<!-- <Button on:click={onClick('requirement', requirement)} type="primary" size="small" icon={SettingsEdit} iconDescription="Edit Configuration" disabled={reqDef.configureComponent == null || !requirement.configuration.actions.update} /> -->
|
|
58
|
+
<div style="display: content" slot="headerRight">
|
|
59
|
+
{@const tags = sharedProgramRequirements[requirement.key]?.length > 1 ? [{ label: 'Shared', onClick: openModal(requirement.key) }] : []}
|
|
60
|
+
<TagSet tags={tags} />
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<ul class="prompts">
|
|
64
|
+
{#each requirement.prompts as prompt (prompt.key)}
|
|
65
|
+
{@const promptDef = uiRegistry.getPrompt(prompt.key)}
|
|
66
|
+
<li class="prompt justify-between">
|
|
67
|
+
<span>
|
|
68
|
+
<Tag type='green'>Prompt</Tag>{prompt.title}
|
|
69
|
+
</span>
|
|
70
|
+
<ActionSet
|
|
71
|
+
actions={[
|
|
72
|
+
// { label: 'View', icon: View },
|
|
73
|
+
{ label: 'settings', icon: SettingsEdit, disabled: promptDef?.configureComponent == null || !prompt.configuration.actions.update, onClick: onClick('prompt', prompt) }
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
</li>
|
|
77
|
+
{/each}
|
|
78
|
+
</ul>
|
|
79
|
+
</Panel>
|
|
80
|
+
{/each}
|
|
81
|
+
</TabContent>
|
|
82
|
+
<TabContent>
|
|
83
|
+
{#each disabledRequirements as requirement (requirement.key)}
|
|
84
|
+
<Panel title={requirement.title} actions={[{ label: 'Enable Requirement', onClick: enablePeriodProgram(requirement.key) }]}>
|
|
85
|
+
Requirement: {requirement.title}
|
|
86
|
+
<ul class="prompts">
|
|
87
|
+
{#each requirement.prompts as prompt (prompt.key)}
|
|
88
|
+
<li class="prompt">
|
|
89
|
+
Prompt: {prompt.title}
|
|
90
|
+
</li>
|
|
91
|
+
{/each}
|
|
92
|
+
</ul>
|
|
93
|
+
</Panel>
|
|
94
|
+
{/each}
|
|
95
|
+
</TabContent>
|
|
96
|
+
</svelte:fragment>
|
|
97
|
+
</Tabs>
|
|
98
|
+
</Panel>
|
|
99
|
+
{/each}
|
|
100
|
+
</Panel>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from "carbon-components-svelte/src/Button/Button.svelte";
|
|
3
|
+
import Launch from 'carbon-icons-svelte/lib/Launch.svelte'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Array of external links to show above the form.
|
|
7
|
+
* There can be up to 3 external links to help applicants if there are things they need to do externally to the app request.
|
|
8
|
+
* @type {{ url: string, label: string }[]}
|
|
9
|
+
*/
|
|
10
|
+
export let externalLinks: { url: string, label: string }[] = []
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* If true, the form will take the full width of its container. If false, it will be constrained to max-w-screen-md.
|
|
14
|
+
* @type {boolean}
|
|
15
|
+
*/
|
|
16
|
+
export let fullWidth = false
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Alignment of the form container. 'center' will center it and 'left' will align it to the left.
|
|
20
|
+
* @type {'left' | 'center'}
|
|
21
|
+
*/
|
|
22
|
+
export let align: 'left' | 'center' = 'center'
|
|
23
|
+
</script>
|
|
24
|
+
{#if externalLinks.length > 0}
|
|
25
|
+
<div class="prompt-intro-links flow max-w-screen-md mx-auto px-6">
|
|
26
|
+
<ul class="flex gap-4 flex-wrap mb-4 justify-center">
|
|
27
|
+
{#each externalLinks.slice(0, 3) as link (link.url)}
|
|
28
|
+
<li><Button kind="ghost" icon={Launch} href={link.url}>{link.label}</Button></li>
|
|
29
|
+
{/each}
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
{/if}
|
|
33
|
+
|
|
34
|
+
<div class="px-6 prompt-form flow" class:max-w-screen-md={!fullWidth} class:w-full={fullWidth} class:mx-auto={align === 'center'}>
|
|
35
|
+
<slot />
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import InlineNotification from "carbon-components-svelte/src/Notification/InlineNotification.svelte";
|
|
3
|
+
import type { PromptDefinition } from '../registry'
|
|
4
|
+
|
|
5
|
+
export let def: PromptDefinition | undefined
|
|
6
|
+
export let appRequestId: string
|
|
7
|
+
export let appData: Record<string, any>
|
|
8
|
+
export let prompt: { key: string, answered: boolean, moot: boolean | null, invalidated: boolean | null, invalidatedReason: string | null }
|
|
9
|
+
export let configData: Record<string, any>
|
|
10
|
+
export let gatheredConfigData: Record<string, any>
|
|
11
|
+
export let showMoot = false
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<svelte:boundary onerror={e => console.error(e)}>
|
|
15
|
+
{#if showMoot && prompt.moot}
|
|
16
|
+
<em>Already disqualified.</em>
|
|
17
|
+
{:else if !prompt.answered}
|
|
18
|
+
<em>Incomplete</em>
|
|
19
|
+
{:else if !def?.displayComponent}
|
|
20
|
+
<em>No display component registered.</em>
|
|
21
|
+
<pre>{JSON.stringify(appData[prompt.key] ?? {}, null, 2)}</pre>
|
|
22
|
+
{:else}
|
|
23
|
+
<svelte:component this={def.displayComponent} {appRequestId} data={appData[prompt.key]} appRequestData={appData} {configData} {gatheredConfigData} />
|
|
24
|
+
{/if}
|
|
25
|
+
{#if prompt.invalidated && (showMoot || !prompt.moot)}
|
|
26
|
+
<InlineNotification kind="warning" title="Correction Needed" subtitle={prompt.invalidatedReason ?? undefined} class="mt-2" lowContrast hideCloseButton />
|
|
27
|
+
{/if}
|
|
28
|
+
{#snippet failed()}
|
|
29
|
+
<div class="error">
|
|
30
|
+
<div>Error Loading Component</div>
|
|
31
|
+
<p>There was an error loading the display component for this prompt.</p>
|
|
32
|
+
</div>
|
|
33
|
+
{/snippet}
|
|
34
|
+
</svelte:boundary>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.error div { font-weight: bold; }
|
|
38
|
+
</style>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { base } from "$app/paths"
|
|
3
|
+
import { ColumnList, FieldDate, FieldMultiselect, Pagination, type ActionItem } from "@txstate-mws/carbon-svelte"
|
|
4
|
+
import { DateTime } from "luxon"
|
|
5
|
+
import View from 'carbon-icons-svelte/lib/View.svelte'
|
|
6
|
+
import DocExport from 'carbon-icons-svelte/lib/DocumentExport.svelte'
|
|
7
|
+
import { downloadCsv } from "../csv"
|
|
8
|
+
import type { AppRequest } from "../typed-client"
|
|
9
|
+
import { pluralize } from "txstate-utils"
|
|
10
|
+
|
|
11
|
+
export let data: AppRequest[]
|
|
12
|
+
export let title: string
|
|
13
|
+
export let subtitle: string
|
|
14
|
+
|
|
15
|
+
const selectedActions = (rows: AppRequest[]): ActionItem[] => [
|
|
16
|
+
{
|
|
17
|
+
label: `Download ${rows.length} ${pluralize('application', rows.length)}`,
|
|
18
|
+
// icon: TrashCan,
|
|
19
|
+
onClick: () => { console.log(rows); downloadCsv(formatCSVData(rows)) }
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
function formatCSVData (d: AppRequest[]) {
|
|
24
|
+
return d.map(d => ({
|
|
25
|
+
Id: d.id,
|
|
26
|
+
Period: d.period.name,
|
|
27
|
+
'TXST ID': d.applicant.otherInfo,
|
|
28
|
+
Name: d.applicant.fullname,
|
|
29
|
+
'Date Submitted': DateTime.fromISO(d.createdAt).toFormat('f').replace(',', ''),
|
|
30
|
+
Benefit: `"${d.applications.map(a => a.title).join(', ')}"`,
|
|
31
|
+
'Last Submitted': DateTime.fromISO(d.updatedAt).toFormat('f').replace(',', '')
|
|
32
|
+
}))
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div class="flow [ p-4 bg-gray-100 ]">
|
|
37
|
+
<h2 class="[ text-lg ]">{title}</h2>
|
|
38
|
+
<p class="[ text-gray-600 ]">{subtitle}</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<ColumnList
|
|
42
|
+
searchable
|
|
43
|
+
filterTitle='Request Filters'
|
|
44
|
+
{selectedActions}
|
|
45
|
+
listActions={[
|
|
46
|
+
{ label: 'Download', icon: DocExport, onClick: () => { console.log(data); downloadCsv(formatCSVData(data)) } }
|
|
47
|
+
]}
|
|
48
|
+
columns={[
|
|
49
|
+
{ id: 'request', label: 'Request #', tags: (row) => [{ label: String(row.id), }] },
|
|
50
|
+
{ id: 'period', label: 'Period', render: r => r.period.name },
|
|
51
|
+
{ id: 'aNumber', label: 'TXST ID' },
|
|
52
|
+
{ id: 'name', label: 'Name', get: 'applicant.fullname' },
|
|
53
|
+
{ id: 'dateSubmitted', label: 'Date Submitted', render: r => DateTime.fromISO(r.createdAt).toFormat('f') },
|
|
54
|
+
{ id: 'benefit', label: 'Benefit', render: r => r.applications.map(a => a.title).join(', ') },
|
|
55
|
+
{ id: 'lastUpdated', label: 'Last Updated', render: r => DateTime.fromISO(r.updatedAt).toFormat('f') },
|
|
56
|
+
]}
|
|
57
|
+
rows={data}
|
|
58
|
+
title="App Requests"
|
|
59
|
+
|
|
60
|
+
actions={r => [
|
|
61
|
+
{
|
|
62
|
+
label: 'View',
|
|
63
|
+
icon: View,
|
|
64
|
+
// href: `${base}/requests/${r.id}/apply`
|
|
65
|
+
href: `${base}/requests/${r.id}/approve`
|
|
66
|
+
}
|
|
67
|
+
]}
|
|
68
|
+
>
|
|
69
|
+
<svelte:fragment slot="filters">
|
|
70
|
+
<FieldMultiselect
|
|
71
|
+
path="period"
|
|
72
|
+
labelText="Period"
|
|
73
|
+
items={[]}
|
|
74
|
+
placeholder="Choose one or more"
|
|
75
|
+
/>
|
|
76
|
+
<FieldDate
|
|
77
|
+
path='dateSubmitted'
|
|
78
|
+
label='Date Submitted'
|
|
79
|
+
/>
|
|
80
|
+
<FieldMultiselect
|
|
81
|
+
path="test"
|
|
82
|
+
labelText="Period"
|
|
83
|
+
items={[]}
|
|
84
|
+
placeholder="Choose one or more"
|
|
85
|
+
/>
|
|
86
|
+
</svelte:fragment>
|
|
87
|
+
</ColumnList>
|
|
88
|
+
|
|
89
|
+
<Pagination
|
|
90
|
+
totalItems={data.length}
|
|
91
|
+
pageSize={25}
|
|
92
|
+
chooseSize
|
|
93
|
+
/>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { BadgeNumber } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
import Accordion from "carbon-components-svelte/src/Accordion/Accordion.svelte";
|
|
4
|
+
import AccordionItem from "carbon-components-svelte/src/Accordion/AccordionItem.svelte";
|
|
5
|
+
|
|
6
|
+
export let items: { id: string, message: string }[] = []
|
|
7
|
+
export let variant: 'warning' | 'error' = 'error'
|
|
8
|
+
export let accordionTitle = 'Multiple items'
|
|
9
|
+
|
|
10
|
+
$: badgeStyle = variant === 'warning'
|
|
11
|
+
? '--badge-bg: var(--yellow-01, #F3D690); --badge-text: #6F510C'
|
|
12
|
+
: '--badge-bg: #FBE9EA; --badge-text: #a11c25'
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#if items.length === 1}
|
|
16
|
+
<div class="flex items-center">
|
|
17
|
+
<BadgeNumber value={1} class="mt-2 mr-2" style={badgeStyle} />
|
|
18
|
+
<p class="mt-2 mb-0 text-sm">{items[0].message}</p>
|
|
19
|
+
</div>
|
|
20
|
+
{:else if items.length > 1}
|
|
21
|
+
<div class="flex">
|
|
22
|
+
<BadgeNumber value={items.length} class="mt-5 mr-2" style={badgeStyle} />
|
|
23
|
+
<div class="mt-2 w-full">
|
|
24
|
+
<Accordion align="start">
|
|
25
|
+
<AccordionItem title={accordionTitle}>
|
|
26
|
+
<ol class="list-decimal">
|
|
27
|
+
{#each items as item (item.id)}
|
|
28
|
+
<li>{item.message}</li>
|
|
29
|
+
{/each}
|
|
30
|
+
</ol>
|
|
31
|
+
</AccordionItem>
|
|
32
|
+
</Accordion>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import WarningAltFilled from "carbon-icons-svelte/lib/WarningAltFilled.svelte";
|
|
3
|
+
import type { ComponentProps } from 'svelte'
|
|
4
|
+
|
|
5
|
+
interface $$Props extends ComponentProps<typeof WarningAltFilled> {}
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div>
|
|
9
|
+
<WarningAltFilled {...$$restProps} />
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
div :global(svg) {
|
|
14
|
+
fill: var(--cds-support-03, rgba(239, 200, 108, 1)) !important;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
div :global([data-icon-path="inner-path"]) {
|
|
18
|
+
fill: black;
|
|
19
|
+
}
|
|
20
|
+
</style>
|