Skip to content

Opt-In / Opt-Out program flow#34

Open
kevinepena wants to merge 27 commits into
txstate-etc:mainfrom
kevinepena:optin-out
Open

Opt-In / Opt-Out program flow#34
kevinepena wants to merge 27 commits into
txstate-etc:mainfrom
kevinepena:optin-out

Conversation

@kevinepena
Copy link
Copy Markdown
Contributor

txstate-etc/reqquest-txstate#267

@kevinepena kevinepena requested a review from a team May 21, 2026 00:50
@saultminer saultminer changed the title #267 Optin out Opt-In / Opt-Out program flow May 21, 2026
<Loading />
{/if}

<Modal
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use a PanelFormDialog here, it will behave a lot better long term since we don't know what downstream might want to do with their prompt. Also much easier to wire up than this was.


<Modal
bind:open
modalHeading={`${optIn ? 'Opt in to' : 'Opt out of'} ${prompt?.title}?`}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't prompt?.title be the program/application title?

on:close={() => { prompt = {} }}
class='opt-out-modal'
>
{#key prompt.id}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This {#key} thing was specific to how the prompt screen was working. You won't need it here.

Comment thread api/src/prompt/prompt.database.ts Outdated
moot: 0 | 1
locked: 0 | 1
invalidated: 0 | 1
optOut: 0 | 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of API-side code hanging around that shouldn't be necessary. More details in other comments.

}), {} as Record<string, string | undefined>)

$: optOutPrograms = applications.reduce((acc, curr) => {
const optOut = curr.requirements.flat().flatMap(r => r.prompts).find(r => r.optOut)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you're depending on the API for which prompt is an optOut prompt and ignoring the optOut in the UI's registry. I suggest:

const optOut = curr.requirements.flat().flatMap(r => r.prompts).find(p => uiRegistry.getPrompt(p.key)?.optOut)

>
{#key prompt.id}
<Form bind:store hideFallbackMessage unsavedWarning submit={onSubmit} validate={onValidate} preloadAsDraft={!prompt.hasSavedData} preload={prompt.preloadData} let:data>
<svelte:component this={def!.formComponent} {data} appRequestId={appRequest.id} appRequestData={appRequest.data} fetched={prompt.fetchedData} configData={prompt.configurationData} gatheredConfigData={prompt.gatheredConfigData} store={store} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why you did it but I wouldn't add the store here without adding it to the other places that render prompt components. We have to present a consistent set of props to the downstream.

I probably wouldn't add it, as it's already in context. You can do this inside a prompt:

import { FORM_CONTEXT, type FormStore } from '@txstate-mws/svelte-forms'
const store = getContext<FormStore>(FORM_CONTEXT)

}, {} as Record<string, OptOutApplication | undefined>)

$: optedOutPrograms = applications.filter(curr => curr.requirements.flat().flatMap(r => r.prompts).find(r => r.optOut)).reduce((acc, c) => {
const optOutRequirement = c.requirements.flat().find(r => r.prompts.find(p => p.optOut))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same dependence on API here. Also r.prompts.some is a little more clear than r.prompts.find. Suggest:

const optOutRequirement = c.requirements.flat().find(r => r.prompts.some(p => uiRegistry.getPrompt(p.key)?.optOut))

description: 'Opt Out',
schema: OptOutSchema,
optOut: true,
prestage: () => ({
Copy link
Copy Markdown
Contributor

@wickning1 wickning1 May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oof, I see why you did this and it's going to be confusing for folks on the regular, but I think the requirement just needs to treat undefined as MET instead of trying to use prestage to inject data into here.

resolve: (data, config) => {
const promptData = data['opt_out_prompt'] as OptOutData
if (promptData?.optOut) return { status: RequirementStatus.DISQUALIFYING }
if (promptData?.optOut == null) return { status: RequirementStatus.PENDING }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I was referring to in the earlier comment. Don't return PENDING here, just return MET or NOT_APPLICABLE if they have never opened the modal.

prestage: () => ({
optOut: false
}),
validate: (data, config) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this validate could be omitted once the requirement is updated to accept undefined as MET. No big deal to include it but I want to be sure it's not needed because that'd be an ugly thing for downstream to have to remember to do.

@wickning1
Copy link
Copy Markdown
Contributor

wickning1 commented May 27, 2026

So, after we spoke yesterday about the problems you were having with the opt-out prompt being marked as UNREACHABLE, I realize that the API probably does have to know about opt-out.

The basic goal that I was pursuing while designing the architecture was to ask only as many questions as we must, to get the applicant approved or denied. So the visibility/unreachable logic makes this assumption: if the resolve function resolves to something non-PENDING before considering the information from your prompt, your prompt was unnecessary, therefore we don't need to ask it and it's marked UNREACHABLE.

An opt-out breaks this assumption, because the requirement's resolve function is MET by default, but you can answer the opt-out prompt to switch it to DISQUALIFYING. So the prompt should remain reachable even though answering it is not required to come to a decision.

I wanted to get that all down for you to improve understanding, but to break it down into something actionable, I think the answer is to move the optOut config to the API's PromptDefinition instead of the UI's. Then you can add it to the Prompt in prompt.model.ts as a @Field() (no need to store it in the database or any of that). My comments about ApplicantProgramList.svelte are no longer valid, you would want to depend on the API to give you the optOut on a prompt.

On the API side, one additional change is needed to look at optOut when deciding to make a prompt UNREACHABLE. I can help with that part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants