Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JN-1601] UX for configuring pre-enroll url options [member's choice] #1442

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ui-admin/src/forms/FormPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
Profile,
surveyJSModelFromFormContent,
useForceUpdate,
useI18n
useI18n,
getSurveyJsAnswerList
} from '@juniper/ui-core'

import { FormPreviewOptions } from './FormPreviewOptions'
Expand Down Expand Up @@ -63,13 +64,15 @@ export const FormPreview = (props: FormPreviewProps) => {
<div className="flex-shrink-0 p-3" style={{ width: 300 }}>
<FormPreviewOptions
value={{
answers: getSurveyJsAnswerList(surveyModel),
ignoreValidation: surveyModel.ignoreValidation,
showInvisibleElements: surveyModel.showInvisibleElements,
locale: surveyModel.locale,
profile: surveyModel.getVariable('profile'),
proxyProfile: surveyModel.getVariable('proxyProfile'),
isGovernedUser: surveyModel.getVariable('isGovernedUser')
}}
forceUpdate={forceUpdate}
onChange={({ ignoreValidation, showInvisibleElements, profile, proxyProfile, isGovernedUser }) => {
surveyModel.ignoreValidation = ignoreValidation
surveyModel.showInvisibleElements = showInvisibleElements
Expand Down
38 changes: 33 additions & 5 deletions ui-admin/src/forms/FormPreviewOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React from 'react'
import { Profile } from '@juniper/ui-core'
import InfoPopup from '../components/forms/InfoPopup'
import { TextInput } from '../components/forms/TextInput'
import React, { useEffect, useState } from 'react'
import { Answer, Profile } from '@juniper/ui-core'
import InfoPopup from 'components/forms/InfoPopup'
import { TextInput } from 'components/forms/TextInput'
import { Button } from 'components/forms/Button'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClipboard } from '@fortawesome/free-solid-svg-icons'

type FormPreviewOptions = {
answers: Answer[]
ignoreValidation: boolean
showInvisibleElements: boolean
locale: string
Expand All @@ -14,12 +18,30 @@ type FormPreviewOptions = {

type FormPreviewOptionsProps = {
value: FormPreviewOptions
forceUpdate: () => void
onChange: (newValue: FormPreviewOptions) => void
}

/** Controls for configuring the form editor's preview tab. */
export const FormPreviewOptions = (props: FormPreviewOptionsProps) => {
const { value, onChange } = props
const { value, forceUpdate, onChange } = props
const [copyTriggered, setCopyTriggered] = useState(false)

//this effect is necessary because the answers typed into the preview do not persist to the model
//until a component re-render is triggered, hence the forceUpdate call in the copy button onClick.
//however, just forcing a re-render is not enough. we also need to ensure that everything happens
//in the right order, so we copy the latest answers to the clipboard only after the re-render.
useEffect(() => {
if (copyTriggered) {
const filteredAnswers = value.answers.filter(a => a.questionStableId !== 'qualified')
filteredAnswers.map(a => {
// @ts-ignore
delete a.format
})
navigator.clipboard.writeText(JSON.stringify(filteredAnswers))
setCopyTriggered(false)
}
}, [copyTriggered])

return (
<div>
Expand Down Expand Up @@ -136,6 +158,12 @@ export const FormPreviewOptions = (props: FormPreviewOptionsProps) => {
unboldLabel={true}
value={value.proxyProfile?.familyName ?? ''} />
</div>
<Button variant="secondary" className="mt-2" onClick={() => {
forceUpdate() //trigger a re-render to persist the answers to the survey model
setCopyTriggered(true)
}}>
<FontAwesomeIcon icon={faClipboard}/> Copy answers
</Button>
</div>

</div>
Expand Down
139 changes: 139 additions & 0 deletions ui-admin/src/study/surveys/PreEnrollShortcutModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React, { useState, useEffect } from 'react'
import Api, {
VersionedForm
} from 'api/api'
import Modal from 'react-bootstrap/Modal'
import { Button, IconButton } from 'components/forms/Button'
import { StudyEnvContextT } from 'study/StudyEnvironmentRouter'
import { TextInput } from 'components/forms/TextInput'
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
import { Checkbox } from 'components/forms/Checkbox'
import queryString from 'query-string'
import { useConfig } from 'providers/ConfigProvider'

type ReferralSource = {
referringSite: string
}

type PreEnrollQueryParams = {
skipPreEnroll?: boolean
referralSource?: ReferralSource
preFilledAnswers?: string
}

/** component for selecting versions of a form */
export default function PreEnrollShortcutModal({
studyEnvContext, workingForm, onDismiss
}: {
studyEnvContext: StudyEnvContextT,
workingForm: VersionedForm,
onDismiss: () => void
}) {
const zoneConfig = useConfig()

const currentPortalEnv = studyEnvContext.portal.portalEnvironments.find(env =>
env.environmentName === studyEnvContext.currentEnv.environmentName)
const portalUrl= Api.getParticipantLink(currentPortalEnv!.portalEnvironmentConfig, zoneConfig.participantUiHostname,
studyEnvContext.portal.shortcode, currentPortalEnv!.environmentName)

const [shortcutUrl, setShortcutUrl] = useState<string | undefined>(portalUrl)
const [queryParams, setQueryParams] = useState<PreEnrollQueryParams>({})
const [skipPreEnroll, setSkipPreEnroll] = useState<boolean>(false)
const [referralSource, setReferralSource] = useState<ReferralSource>()
const [preFilledAnswers, setPrefilledAnswers] = useState<string>()

useEffect(() => {
const params = {
...queryParams,
referralSource: queryParams.referralSource ? JSON.stringify(queryParams.referralSource) : undefined,
preFilledAnswers: queryParams.preFilledAnswers ? JSON.stringify(queryParams.preFilledAnswers) : undefined
}

const allParamsEmpty = Object.values(params).every(v => v === undefined)
setShortcutUrl(allParamsEmpty ? portalUrl : `${portalUrl}?${queryString.stringify(params)}`)
}, [queryParams])

return <Modal show={true} onHide={onDismiss} size="lg">
<Modal.Header closeButton>
<Modal.Title>{workingForm.name} - shortcuts</Modal.Title>
</Modal.Header>
<Modal.Body>
<form>
<p>
Participants can be directed to your website with pre-configured enrollment options.
After configuring the options below, copy and paste the URL to
direct participants to the portal with your selected options.
</p>
<div className={'my-3'}>
<TextInput
type="text"
infoContent={
'If set, any participant signing up with this link will have this ' +
'site recorded as the referring site in their profile. Use this option if you want to track ' +
'participant enrollments from a partner website or other source such as a newsletter.'
}
label={'Referring Site'}
value={referralSource?.referringSite} onChange={e => {
const newReferralSource = e ? { referringSite: e } : undefined
setReferralSource(newReferralSource)
setQueryParams(prev => ({
...prev,
referralSource: newReferralSource
}))
}}/>
</div>

<div className={'my-3'}>
<TextInput
type="text"
infoContent={
`If set, any participant signing up with this link will have these answers
pre-filled in the pre-enroll survey. Go to the "Preview" tab in the survey
builder and fill out the answers you want to pre-fill. Then, click the
"Copy answers" button to copy the answers to the clipboard.
Finally, paste that value here. `
}
label={'Pre-filled Answers'}
value={preFilledAnswers} onChange={e => {
setPrefilledAnswers(e)
setQueryParams(prev => ({
...prev,
preFilledAnswers: e
}))
}}/>
</div>

<Checkbox
label={'Skip Pre-enroll'} checked={skipPreEnroll}
infoContent={
'If checked, any participant signing up with this link will bypass the pre-enroll survey entirely.'
}
onClick={() => {
setSkipPreEnroll(!skipPreEnroll)
setQueryParams(prev => ({
...prev,
skipPreEnroll: !skipPreEnroll ? true : undefined
}))
}}/>

<div className={'bg-light mt-3 px-2 py-2 border rounded'}>
<div>
<span className={'fw-semibold'}>Configured Portal URL</span>
<div className={'d-flex mb-3'}>
<TextInput type="text" value={shortcutUrl} onChange={e => setShortcutUrl(e)}/>
<IconButton
aria-label={'Copy to clipboard'}
icon={faClipboard}
onClick={() => navigator.clipboard.writeText(shortcutUrl || 'error: could not create url')}/>
</div>
</div>
</div>
</form>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onDismiss}>
Done
</Button>
</Modal.Footer>
</Modal>
}
13 changes: 13 additions & 0 deletions ui-admin/src/study/surveys/SurveyEditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import FormHistoryModal from './FormHistoryModal'
import useLanguageSelectorFromParam from 'portal/languages/useLanguageSelector'
import Select from 'react-select'
import InfoPopup from 'components/forms/InfoPopup'
import PreEnrollShortcutModal from './PreEnrollShortcutModal'

type SurveyEditorViewProps = {
studyEnvContext: StudyEnvContextT
Expand Down Expand Up @@ -52,6 +53,7 @@ const SurveyEditorView = (props: SurveyEditorViewProps) => {
const [showLoadedDraftModal, setShowLoadedDraftModal] = useState(!!getDraft({ formDraftKey: FORM_DRAFT_KEY }))
const [showDiscardDraftModal, setShowDiscardDraftModal] = useState(false)
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false)
const [showPreEnrollShortcuts, setShowPreEnrollShortcuts] = useState(false)
const [showVersionSelector, setShowVersionSelector] = useState(false)
const [showErrors, setShowErrors] = useState(false)

Expand Down Expand Up @@ -228,6 +230,12 @@ const SurveyEditorView = (props: SurveyEditorViewProps) => {
Download form JSON
</button>
</li>
{ (currentForm as Survey).surveyType !== 'PRE_ENROLL' && <li>
MatthewBemis marked this conversation as resolved.
Show resolved Hide resolved
<button className="dropdown-item"
onClick={() => setShowPreEnrollShortcuts(true)}>
Pre-enroll Shortcuts
</button>
</li> }
</ul>
</div>
{ showVersionSelector && <FormHistoryModal
Expand All @@ -244,6 +252,11 @@ const SurveyEditorView = (props: SurveyEditorViewProps) => {
}}
onDismiss={() => setShowAdvancedOptions(false)}/>
}
{ showPreEnrollShortcuts && <PreEnrollShortcutModal
studyEnvContext={studyEnvContext}
workingForm={{ ...currentForm, ...draft }}
onDismiss={() => setShowPreEnrollShortcuts(false)}/>
}
</div>
<ApiProvider api={previewApi(portal.shortcode, currentEnv.environmentName)}>
<FormContentEditor
Expand Down
Loading