@tellescope/react-components 1.227.0 → 1.229.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/lib/cjs/Forms/forms.v2.d.ts +116 -0
- package/lib/cjs/Forms/forms.v2.d.ts.map +1 -0
- package/lib/cjs/Forms/forms.v2.js +760 -0
- package/lib/cjs/Forms/forms.v2.js.map +1 -0
- package/lib/cjs/Forms/hooks.d.ts.map +1 -1
- package/lib/cjs/Forms/hooks.js +8 -3
- package/lib/cjs/Forms/hooks.js.map +1 -1
- package/lib/cjs/Forms/index.d.ts +1 -0
- package/lib/cjs/Forms/index.d.ts.map +1 -1
- package/lib/cjs/Forms/index.js +6 -0
- package/lib/cjs/Forms/index.js.map +1 -1
- package/lib/cjs/Forms/inputs.v2.d.ts +81 -0
- package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -0
- package/lib/cjs/Forms/inputs.v2.js +2289 -0
- package/lib/cjs/Forms/inputs.v2.js.map +1 -0
- package/lib/cjs/Forms/localization.d.ts.map +1 -1
- package/lib/cjs/Forms/localization.js +3 -0
- package/lib/cjs/Forms/localization.js.map +1 -1
- package/lib/cjs/Forms/types.d.ts +1 -0
- package/lib/cjs/Forms/types.d.ts.map +1 -1
- package/lib/cjs/state.d.ts +34 -0
- package/lib/cjs/state.d.ts.map +1 -1
- package/lib/cjs/state.js +16 -2
- package/lib/cjs/state.js.map +1 -1
- package/lib/esm/Forms/forms.v2.d.ts +116 -0
- package/lib/esm/Forms/forms.v2.d.ts.map +1 -0
- package/lib/esm/Forms/forms.v2.js +725 -0
- package/lib/esm/Forms/forms.v2.js.map +1 -0
- package/lib/esm/Forms/hooks.d.ts.map +1 -1
- package/lib/esm/Forms/hooks.js +8 -3
- package/lib/esm/Forms/hooks.js.map +1 -1
- package/lib/esm/Forms/index.d.ts +1 -0
- package/lib/esm/Forms/index.d.ts.map +1 -1
- package/lib/esm/Forms/index.js +2 -0
- package/lib/esm/Forms/index.js.map +1 -1
- package/lib/esm/Forms/inputs.v2.d.ts +81 -0
- package/lib/esm/Forms/inputs.v2.d.ts.map +1 -0
- package/lib/esm/Forms/inputs.v2.js +2218 -0
- package/lib/esm/Forms/inputs.v2.js.map +1 -0
- package/lib/esm/Forms/localization.d.ts.map +1 -1
- package/lib/esm/Forms/localization.js +3 -0
- package/lib/esm/Forms/localization.js.map +1 -1
- package/lib/esm/Forms/types.d.ts +1 -0
- package/lib/esm/Forms/types.d.ts.map +1 -1
- package/lib/esm/state.d.ts +34 -0
- package/lib/esm/state.d.ts.map +1 -1
- package/lib/esm/state.js +13 -0
- package/lib/esm/state.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/Forms/forms.v2.tsx +1321 -0
- package/src/Forms/hooks.tsx +10 -5
- package/src/Forms/index.ts +5 -2
- package/src/Forms/inputs.v2.tsx +3869 -0
- package/src/Forms/localization.ts +1 -0
- package/src/Forms/types.ts +1 -0
- package/src/state.tsx +25 -5
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import { Button, CircularProgress, FileBlob, FileUploadHandler, Flex, LinearProgress, LoadingButton, Modal, Paper, Styled, Typography, WithTheme, form_display_text_for_language, useFileUpload, useFormResponses, useSession } from "../index"
|
|
3
|
+
import { useListForFormFields, useOrganizationTheme, useTellescopeForm, WithOrganizationTheme, Response, FileResponse, NextFieldLogicOptions } from "./hooks"
|
|
4
|
+
import { ChangeHandler, FormInputs } from "./types"
|
|
5
|
+
import { AddToDatabaseProps, AddressInput, AllergiesInput, AppointmentBookingInput, ChargeebeeInput, ConditionsInput, DatabaseSelectInput, DateInput, DateStringInput, DropdownInput, EmailInput, EmotiiInput, FileInput, FilesInput, HeightInput, HiddenValueInput, InsuranceInput, LanguageSelect, MedicationsInput, MultipleChoiceInput, NumberInput, PhoneInput, Progress, RankingInput, RatingInput, RedirectInput, RelatedContactsInput, RichTextInput, SignatureInput, StringInput, StringLongInput, StripeInput, TableInput, TimeInput, TimezoneInput, defaultButtonStyles } from "./inputs.v2"
|
|
6
|
+
import { PRIMARY_HEX } from "@tellescope/constants"
|
|
7
|
+
import { FormResponse, FormField, Form, Enduser } from "@tellescope/types-client"
|
|
8
|
+
import { FormResponseAnswerFileValue, OrganizationTheme } from "@tellescope/types-models"
|
|
9
|
+
import { calculate_form_scoring, field_can_autosubmit, form_response_value_to_string, formatted_date, object_is_empty, objects_equivalent, read_local_storage, remove_script_tags, responses_satisfy_conditions, truncate_string } from "@tellescope/utilities"
|
|
10
|
+
import { Divider } from "@mui/material"
|
|
11
|
+
|
|
12
|
+
export const TellescopeFormContainerV2 = ({ businessId, organizationIds, ...props } : {
|
|
13
|
+
businessId?: string,
|
|
14
|
+
organizationIds?: string[],
|
|
15
|
+
dontAddContext?: boolean,
|
|
16
|
+
children: React.ReactNode,
|
|
17
|
+
hideBg?: boolean,
|
|
18
|
+
backgroundColor?: string,
|
|
19
|
+
hideLogo?: boolean,
|
|
20
|
+
logoHeight?: number,
|
|
21
|
+
language?: string,
|
|
22
|
+
onChangeLanguage?: (l: string) => void,
|
|
23
|
+
paperMinHeight?: React.CSSProperties['minHeight'],
|
|
24
|
+
maxWidth?: number,
|
|
25
|
+
} & Styled) => {
|
|
26
|
+
// if context already is provided, no need to duplicate
|
|
27
|
+
if (props.dontAddContext) return (
|
|
28
|
+
<TellescopeFormContainerWithThemeV2 {...props} />
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<WithOrganizationTheme businessId={businessId} organizationIds={organizationIds}>
|
|
33
|
+
<TellescopeFormContainerWithThemeV2 {...props} />
|
|
34
|
+
</WithOrganizationTheme>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TellescopeFormContainerWithThemeV2: typeof TellescopeFormContainerV2 = ({ paperMinHeight=575, children, language, onChangeLanguage, style, hideBg, backgroundColor, hideLogo, logoHeight, maxWidth }) => {
|
|
39
|
+
const theme = useOrganizationTheme()
|
|
40
|
+
|
|
41
|
+
// V2: No paper background by default, cleaner layout with light blue background
|
|
42
|
+
// Ignore theme colors or white backgrounds - use our V2 default light blue
|
|
43
|
+
const shouldUseCustomBg = backgroundColor && backgroundColor !== theme.themeColor && backgroundColor !== '#ffffff'
|
|
44
|
+
const finalBgColor = shouldUseCustomBg ? backgroundColor : '#F4F3FA'
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Flex flex={1} column alignItems="center" style={{ backgroundColor: finalBgColor, overflow: 'hidden', paddingTop: 40, paddingBottom: 40, ...style }}>
|
|
48
|
+
<Flex flex={1} column style={{ padding: '0 20px', maxWidth: maxWidth ?? 650, minWidth: 250, width: '100%', height: '100%' }}>
|
|
49
|
+
{language && onChangeLanguage &&
|
|
50
|
+
<Flex style={{ marginTop: 22 }}>
|
|
51
|
+
<LanguageSelect value={language} onChange={onChangeLanguage} />
|
|
52
|
+
</Flex>
|
|
53
|
+
}
|
|
54
|
+
{children}
|
|
55
|
+
</Flex>
|
|
56
|
+
</Flex>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TellescopeFormProps extends ReturnType<typeof useTellescopeForm> {
|
|
61
|
+
form?: Form,
|
|
62
|
+
isPreview?: boolean,
|
|
63
|
+
onSuccess?: (r: FormResponse) => void,
|
|
64
|
+
customInputs?: FormInputs
|
|
65
|
+
submitted?: boolean,
|
|
66
|
+
thanksMessage?: string,
|
|
67
|
+
htmlThanksMessage?: string,
|
|
68
|
+
showSaveDraft?: boolean,
|
|
69
|
+
formTitle?: string,
|
|
70
|
+
repeats: Record<string, string | number>
|
|
71
|
+
backgroundColor?: string,
|
|
72
|
+
rootResponseId?: string,
|
|
73
|
+
parentResponseId?: string,
|
|
74
|
+
downloadComponent?: React.ReactNode,
|
|
75
|
+
enduser?: Partial<Enduser>,
|
|
76
|
+
groupId?: string,
|
|
77
|
+
groupInstance?: string,
|
|
78
|
+
logoHeight?: number,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const LOGO_HEIGHT = 40
|
|
82
|
+
export const TellescopeFormV2 = (props : TellescopeFormProps & Styled & { hideBg?: boolean, theme?: OrganizationTheme, inputStyle?: React.CSSProperties }) => (
|
|
83
|
+
<WithOrganizationTheme>
|
|
84
|
+
<TellescopeFormWithContextV2 {...props} logoHeight={props?.logoHeight || props?.form?.customization?.logoHeight} />
|
|
85
|
+
</WithOrganizationTheme>
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
export const QuestionForField = ({
|
|
89
|
+
form,
|
|
90
|
+
value,
|
|
91
|
+
field,
|
|
92
|
+
file,
|
|
93
|
+
responses,
|
|
94
|
+
selectedFiles,
|
|
95
|
+
onAddFile,
|
|
96
|
+
onFieldChange,
|
|
97
|
+
customInputs,
|
|
98
|
+
fields,
|
|
99
|
+
validateField,
|
|
100
|
+
repeats,
|
|
101
|
+
onRepeatsChange,
|
|
102
|
+
setCustomerId,
|
|
103
|
+
handleDatabaseSelect,
|
|
104
|
+
enduser,
|
|
105
|
+
goToPreviousField,
|
|
106
|
+
isPreviousDisabled,
|
|
107
|
+
enduserId,
|
|
108
|
+
formResponseId,
|
|
109
|
+
submit,
|
|
110
|
+
groupId,
|
|
111
|
+
groupInstance,
|
|
112
|
+
goToNextField,
|
|
113
|
+
spacing,
|
|
114
|
+
isSinglePage,
|
|
115
|
+
rootResponseId,
|
|
116
|
+
isInQuestionGroup,
|
|
117
|
+
logicOptions,
|
|
118
|
+
uploadingFiles, setUploadingFiles, handleFileUpload,
|
|
119
|
+
groupFields,
|
|
120
|
+
AddToDatabase,
|
|
121
|
+
} : {
|
|
122
|
+
spacing?: number,
|
|
123
|
+
form?: Form,
|
|
124
|
+
repeats: Record<string, string | number>,
|
|
125
|
+
onRepeatsChange: (v: Record<string, string | number>) => void,
|
|
126
|
+
value: Response,
|
|
127
|
+
file: FileResponse,
|
|
128
|
+
field: FormField,
|
|
129
|
+
setCustomerId: React.Dispatch<React.SetStateAction<string | undefined>>,
|
|
130
|
+
isSinglePage?: boolean,
|
|
131
|
+
isInQuestionGroup?: boolean,
|
|
132
|
+
questionGroupSize?: number,
|
|
133
|
+
logicOptions?: NextFieldLogicOptions,
|
|
134
|
+
handleFileUpload: (blob: FileBlob, fieldId: string) => Promise<any>,
|
|
135
|
+
uploadingFiles: { fieldId: string }[]
|
|
136
|
+
setUploadingFiles: React.Dispatch<React.SetStateAction<{ fieldId: string }[]>>,
|
|
137
|
+
groupFields?: FormField[],
|
|
138
|
+
AddToDatabase?: React.JSXElementConstructor<AddToDatabaseProps>,
|
|
139
|
+
} & Pick<TellescopeFormProps, "rootResponseId" | "goToNextField" | "groupId" | "groupInstance" | "submit" | "formResponseId" | 'enduserId' | 'isPreviousDisabled' | 'goToPreviousField' | 'enduser' | 'handleDatabaseSelect' | 'onAddFile' | 'onFieldChange' | 'fields' | 'customInputs' | 'responses' | 'selectedFiles' | 'validateField'>) => {
|
|
140
|
+
const String = customInputs?.['string'] ?? StringInput
|
|
141
|
+
const StringLong = customInputs?.['stringLong'] ?? StringLongInput
|
|
142
|
+
const Email = customInputs?.['email'] ?? EmailInput
|
|
143
|
+
const Number = customInputs?.['number'] ?? NumberInput
|
|
144
|
+
const Phone = customInputs?.['phone'] ?? PhoneInput
|
|
145
|
+
const ResolvedDateInput = customInputs?.['date'] ?? DateInput
|
|
146
|
+
const Signature = customInputs?.['signature'] ?? SignatureInput
|
|
147
|
+
const MultipleChoice = customInputs?.['multiple_choice'] ?? MultipleChoiceInput
|
|
148
|
+
const Stripe = customInputs?.['Stripe'] ?? StripeInput
|
|
149
|
+
const Chargebee = customInputs?.['Chargebee'] ?? ChargeebeeInput
|
|
150
|
+
const File = customInputs?.['file'] ?? FileInput
|
|
151
|
+
const Files = customInputs?.['files'] ?? FilesInput
|
|
152
|
+
const Ranking = customInputs?.['ranking'] ?? RankingInput
|
|
153
|
+
const Rating = customInputs?.['rating'] ?? RatingInput
|
|
154
|
+
const Address = customInputs?.['Address'] ?? AddressInput
|
|
155
|
+
const Time = customInputs?.['Time'] ?? TimeInput
|
|
156
|
+
const TimeZone = customInputs?.['Timezone'] ?? TimezoneInput
|
|
157
|
+
const Dropdown = customInputs?.['Dropdown'] ?? DropdownInput
|
|
158
|
+
const DatabaseSelect = customInputs?.['Database Select'] ?? DatabaseSelectInput
|
|
159
|
+
const Medications = customInputs?.['Medications'] ?? MedicationsInput
|
|
160
|
+
const RelatedContacts = customInputs?.['Related Contacts'] ?? RelatedContactsInput
|
|
161
|
+
const Insurance = customInputs?.['Insurance'] ?? InsuranceInput
|
|
162
|
+
const AppointmentBooking = customInputs?.['Appointment Booking'] ?? AppointmentBookingInput
|
|
163
|
+
const Height = customInputs?.['Height'] ?? HeightInput
|
|
164
|
+
const Redirect = customInputs?.['Redirect'] ?? RedirectInput
|
|
165
|
+
const HiddenValue = customInputs?.['Hidden Value'] ?? HiddenValueInput
|
|
166
|
+
const Emotii = customInputs?.['Emotii'] ?? EmotiiInput
|
|
167
|
+
const Allergies = customInputs?.['Allergies'] ?? AllergiesInput
|
|
168
|
+
const Conditions = customInputs?.['Conditions'] ?? ConditionsInput
|
|
169
|
+
const RichText = customInputs?.['Rich Text'] ?? RichTextInput
|
|
170
|
+
|
|
171
|
+
const validationMessage = validateField(field)
|
|
172
|
+
|
|
173
|
+
const feedback = useMemo(() => (
|
|
174
|
+
(field.feedback || [])
|
|
175
|
+
.filter(({ display, ifEquals }) => (
|
|
176
|
+
ifEquals === value.answer.value
|
|
177
|
+
|| (Array.isArray(value.answer.value) && value.answer.value.includes(ifEquals as any))
|
|
178
|
+
))
|
|
179
|
+
.map(v => v.display)
|
|
180
|
+
), [field.feedback, value])
|
|
181
|
+
|
|
182
|
+
if (!value) return null
|
|
183
|
+
if (
|
|
184
|
+
isInQuestionGroup
|
|
185
|
+
&& field.groupShowCondition && !object_is_empty(field.groupShowCondition)
|
|
186
|
+
&& !responses_satisfy_conditions(responses, field.groupShowCondition, logicOptions)
|
|
187
|
+
) {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
// margin leaves room for error message in Question Group
|
|
192
|
+
<Flex column flex={1} style={{ marginBottom: spacing ?? 25 }} id={field.id}>
|
|
193
|
+
{field.type !== 'Redirect' && field.title &&
|
|
194
|
+
<Typography component="h4" style={{
|
|
195
|
+
marginTop: 15, // ensures PDF display doesn't push description into overlap with logo / title at top of form
|
|
196
|
+
marginBottom: 14,
|
|
197
|
+
fontSize: field.titleFontSize || (field.type === 'Question Group' ? 22 : 20),
|
|
198
|
+
fontWeight: field.type === 'Question Group' ? 'bold' : undefined,
|
|
199
|
+
}}>
|
|
200
|
+
{field.title}{!(field.isOptional || field.type === 'description' || field.type === 'Question Group' || field.type === 'Insurance') ? '*' : ''}
|
|
201
|
+
</Typography>
|
|
202
|
+
}
|
|
203
|
+
{!field.title && (field.type === 'Question Group' || field.type === 'signature') && !form?.customization?.hideLogo &&
|
|
204
|
+
// ensures PDF display doesn't push description into overlap with logo / title at top of form
|
|
205
|
+
// also ensures spacing between logo and question group
|
|
206
|
+
<div style={{ marginTop: 15 }}></div>
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
{feedback.length > 0 &&
|
|
210
|
+
<Flex column style={{ marginBottom: 11, marginTop: 3, }}>
|
|
211
|
+
{feedback.map((f, i) => (
|
|
212
|
+
<Typography key={i} color="error" style={{ fontSize: 20 }}>
|
|
213
|
+
{f}
|
|
214
|
+
</Typography>
|
|
215
|
+
))}
|
|
216
|
+
</Flex>
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
{
|
|
220
|
+
// If field has pre-populated value and is set to be disabled when pre-populated, show as underlined text
|
|
221
|
+
field.disabledWhenPrepopulated && value.answer.value !== undefined && value.answer.value !== null && value.answer.value !== '' ? (
|
|
222
|
+
<div
|
|
223
|
+
style={{
|
|
224
|
+
padding: '8px 0',
|
|
225
|
+
borderBottom: '1px solid rgba(0, 0, 0, 0.42)',
|
|
226
|
+
width: '100%'
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<Typography
|
|
230
|
+
style={{
|
|
231
|
+
fontSize: '1rem',
|
|
232
|
+
color: 'rgba(0, 0, 0, 0.87)',
|
|
233
|
+
whiteSpace: 'pre-wrap'
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{form_response_value_to_string(value.answer.value)}
|
|
237
|
+
</Typography>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
: field.type === 'file' ? (
|
|
241
|
+
<File field={field} value={file.blobs?.[0] as any} onChange={onAddFile as any} form={form}
|
|
242
|
+
existingFileName={
|
|
243
|
+
value.answer.type === 'file'
|
|
244
|
+
? value.answer.value?.name
|
|
245
|
+
: ''
|
|
246
|
+
}
|
|
247
|
+
handleFileUpload={handleFileUpload} uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
|
|
248
|
+
/>
|
|
249
|
+
)
|
|
250
|
+
: field.type === 'files' ? (
|
|
251
|
+
<Files field={field} value={file.blobs as any} onChange={onAddFile as any} form={form}
|
|
252
|
+
// existingFileName={
|
|
253
|
+
// value.answer.type === 'files'
|
|
254
|
+
// ? value.answer.value?.name
|
|
255
|
+
// : ''
|
|
256
|
+
// }
|
|
257
|
+
handleFileUpload={handleFileUpload} uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
|
|
258
|
+
/>
|
|
259
|
+
)
|
|
260
|
+
: field.type === 'dateString' ? (
|
|
261
|
+
<DateStringInput field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'string'>} form={form} />
|
|
262
|
+
)
|
|
263
|
+
: field.type === 'Hidden Value' ? (
|
|
264
|
+
<HiddenValue groupFields={groupFields} isSinglePage={isSinglePage} goToNextField={goToNextField} goToPreviousField={goToPreviousField} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} form={form} />
|
|
265
|
+
)
|
|
266
|
+
: field.type === 'Address' ? (
|
|
267
|
+
<Address field={field} disabled={value.disabled} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form} />
|
|
268
|
+
)
|
|
269
|
+
: field.type === 'Emotii' ? (
|
|
270
|
+
<Emotii enduser={enduser} enduserId={enduserId} field={field} disabled={value.disabled} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form} />
|
|
271
|
+
)
|
|
272
|
+
: field.type === 'Allergies' ? (
|
|
273
|
+
<Allergies enduser={enduser} enduserId={enduserId} field={field} disabled={value.disabled} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form} />
|
|
274
|
+
)
|
|
275
|
+
: field.type === 'Conditions' ? (
|
|
276
|
+
<Conditions enduser={enduser} enduserId={enduserId} field={field} disabled={value.disabled} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form} />
|
|
277
|
+
)
|
|
278
|
+
: field.type === 'Height' ? (
|
|
279
|
+
<Height field={field} disabled={value.disabled} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form} />
|
|
280
|
+
)
|
|
281
|
+
: field.type === 'Redirect' ? (
|
|
282
|
+
<Redirect enduserId={enduserId} rootResponseId={rootResponseId} formResponseId={formResponseId} responses={responses} enduser={enduser} groupId={groupId} groupInsance={groupInstance} submit={submit} field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form} />
|
|
283
|
+
)
|
|
284
|
+
: field.type === 'Related Contacts' ? (
|
|
285
|
+
<RelatedContacts field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form} />
|
|
286
|
+
)
|
|
287
|
+
: field.type === 'string' ? (
|
|
288
|
+
<String field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'string'>} form={form} error={!!validationMessage && (!['A response is required', 'A value must be checked', 'A file is required', 'Enter a valid phone number', 'Insurer is required'].includes(validationMessage) || value.touched)} />
|
|
289
|
+
)
|
|
290
|
+
: field.type === 'Appointment Booking' ? (
|
|
291
|
+
<AppointmentBooking formResponseId={formResponseId} enduserId={enduserId} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} responses={responses} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Appointment Booking'>} form={form} />
|
|
292
|
+
)
|
|
293
|
+
: field.type === 'Stripe' ? (
|
|
294
|
+
<Stripe enduserId={enduserId} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} />
|
|
295
|
+
)
|
|
296
|
+
: field.type === 'Chargebee' ? (
|
|
297
|
+
<Chargebee field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Chargebee'>} setCustomerId={setCustomerId} form={form} />
|
|
298
|
+
)
|
|
299
|
+
: field.type === 'stringLong' ? (
|
|
300
|
+
<StringLong field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'string' | 'stringLong'>} form={form} error={!!validationMessage && (!['A response is required', 'A value must be checked', 'A file is required', 'Enter a valid phone number', 'Insurer is required'].includes(validationMessage) || value.touched)} />
|
|
301
|
+
)
|
|
302
|
+
: field.type === 'Rich Text' ? (
|
|
303
|
+
<RichText field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'Rich Text'>} form={form} />
|
|
304
|
+
)
|
|
305
|
+
: field.type === 'email' ? (
|
|
306
|
+
<Email field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'email'>} form={form} error={!!validationMessage && (!['A response is required', 'A value must be checked', 'A file is required', 'Enter a valid phone number', 'Insurer is required'].includes(validationMessage) || value.touched)} />
|
|
307
|
+
)
|
|
308
|
+
: field.type === 'number' ? (
|
|
309
|
+
<Number field={field} disabled={value.disabled} value={value.answer.value as number} onChange={onFieldChange as ChangeHandler<'number'>} form={form} error={!!validationMessage && (!['A response is required', 'A value must be checked', 'A file is required', 'Enter a valid phone number', 'Insurer is required'].includes(validationMessage) || value.touched)} />
|
|
310
|
+
)
|
|
311
|
+
: field.type === 'phone' ? (
|
|
312
|
+
<Phone field={field} disabled={value.disabled} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<'phone'>} form={form} error={!!validationMessage && (!['A response is required', 'A value must be checked', 'A file is required', 'Enter a valid phone number', 'Insurer is required'].includes(validationMessage) || value.touched)} />
|
|
313
|
+
)
|
|
314
|
+
: field.type === 'date' ? (
|
|
315
|
+
<ResolvedDateInput field={field} disabled={value.disabled} value={value.answer.value ? new Date(value.answer.value as string | Date) : undefined} onChange={onFieldChange as ChangeHandler<'date'>} form={form} />
|
|
316
|
+
)
|
|
317
|
+
: field.type === 'signature' ? (
|
|
318
|
+
<Signature enduser={enduser} field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'signature'>} form={form}/>
|
|
319
|
+
)
|
|
320
|
+
: field.type === 'multiple_choice' ? (
|
|
321
|
+
<MultipleChoice field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'multiple_choice'>} form={form}/>
|
|
322
|
+
)
|
|
323
|
+
: field.type === 'Dropdown' ? (
|
|
324
|
+
<Dropdown field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Dropdown'>} form={form}/>
|
|
325
|
+
)
|
|
326
|
+
: field.type === 'Database Select' ? (
|
|
327
|
+
<DatabaseSelect field={field} disabled={value.disabled} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Database Select'>}
|
|
328
|
+
onDatabaseSelect={handleDatabaseSelect}
|
|
329
|
+
responses={responses} form={form}
|
|
330
|
+
AddToDatabase={AddToDatabase}
|
|
331
|
+
/>
|
|
332
|
+
)
|
|
333
|
+
: field.type === 'Medications' ? (
|
|
334
|
+
<Medications field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Medications'>} form={form}/>
|
|
335
|
+
)
|
|
336
|
+
: field.type === 'Insurance' ? (
|
|
337
|
+
<Insurance field={field} value={value.answer.value as any} form={form}
|
|
338
|
+
onDatabaseSelect={handleDatabaseSelect}
|
|
339
|
+
enduser={enduser} responses={responses} // for filtering insurers by state
|
|
340
|
+
onChange={(v, fieldId) => (onFieldChange as ChangeHandler<'Insurance'>)({
|
|
341
|
+
...v,
|
|
342
|
+
relationship: v?.relationship || 'Self', // make sure relationship is initialized to self if input is provided
|
|
343
|
+
}, fieldId)}
|
|
344
|
+
/>
|
|
345
|
+
)
|
|
346
|
+
: field.type === 'rating' ? (
|
|
347
|
+
<Rating field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'rating'>} form={form}/>
|
|
348
|
+
)
|
|
349
|
+
: field.type === 'ranking' ? (
|
|
350
|
+
<Ranking field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'ranking'>} form={form}/>
|
|
351
|
+
)
|
|
352
|
+
: field.type === 'Table Input' ? (
|
|
353
|
+
<TableInput field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form}/>
|
|
354
|
+
)
|
|
355
|
+
: field.type === 'Timezone' ? (
|
|
356
|
+
<TimezoneInput field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Timezone'>} form={form}/>
|
|
357
|
+
)
|
|
358
|
+
: field.type === 'Time' ? (
|
|
359
|
+
<Time field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<any>} form={form}/>
|
|
360
|
+
)
|
|
361
|
+
: field.type === 'Question Group' ? (
|
|
362
|
+
<Flex column flex={1}>
|
|
363
|
+
{(field.options?.subFields ?? []).map(({ id }, indexInGroup) => {
|
|
364
|
+
const match = fields.find(f => f.id === id)
|
|
365
|
+
if (!match) return null
|
|
366
|
+
|
|
367
|
+
const value = responses.find(r => r.fieldId === match.id)
|
|
368
|
+
const file = selectedFiles.find(r => r.fieldId === match.id)
|
|
369
|
+
if (!value) return null
|
|
370
|
+
if (!file) return null
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<Flex key={id} flex={1}>
|
|
374
|
+
<QuestionForField groupId={groupId} groupInstance={groupInstance} customInputs={customInputs} field={match} fields={fields} handleDatabaseSelect={handleDatabaseSelect}
|
|
375
|
+
enduser={enduser} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} goToNextField={goToNextField}
|
|
376
|
+
form={form} formResponseId={formResponseId} rootResponseId={rootResponseId} submit={submit}
|
|
377
|
+
repeats={repeats} onRepeatsChange={onRepeatsChange} setCustomerId={setCustomerId}
|
|
378
|
+
value={value} file={file}
|
|
379
|
+
onAddFile={onAddFile} onFieldChange={onFieldChange}
|
|
380
|
+
responses={responses} selectedFiles={selectedFiles}
|
|
381
|
+
validateField={validateField} enduserId={enduserId}
|
|
382
|
+
spacing={field.options?.groupPadding}
|
|
383
|
+
logicOptions={logicOptions}
|
|
384
|
+
isInQuestionGroup
|
|
385
|
+
groupFields={
|
|
386
|
+
fields.filter(f => field.options?.subFields?.find(s => s.id === f.id))
|
|
387
|
+
}
|
|
388
|
+
questionGroupSize={field.options?.subFields?.length}
|
|
389
|
+
uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
|
|
390
|
+
handleFileUpload={handleFileUpload}
|
|
391
|
+
AddToDatabase={AddToDatabase}
|
|
392
|
+
/>
|
|
393
|
+
</Flex>
|
|
394
|
+
)
|
|
395
|
+
})}
|
|
396
|
+
</Flex>
|
|
397
|
+
)
|
|
398
|
+
: null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
{field.options?.repeat && (
|
|
402
|
+
<Flex style={{ marginTop: '8px' }}>
|
|
403
|
+
{
|
|
404
|
+
field.type === 'string' ? (
|
|
405
|
+
<String field={field} label="Repeat" value={repeats[field.id] as string ?? ''} onChange={u => onRepeatsChange({ ...repeats, [field.id]: u! })} form={form} />
|
|
406
|
+
)
|
|
407
|
+
: field.type === 'stringLong' ? (
|
|
408
|
+
<StringLong field={field} label="Repeat" value={repeats[field.id] as string ?? ''} onChange={u => onRepeatsChange({ ...repeats, [field.id]: u! })} form={form} />
|
|
409
|
+
)
|
|
410
|
+
: field.type === 'number' ? (
|
|
411
|
+
<Number field={field} label="Repeat" value={repeats[field.id] as number ?? ''} onChange={u => onRepeatsChange({ ...repeats, [field.id]: u! })} form={form} />
|
|
412
|
+
)
|
|
413
|
+
: null
|
|
414
|
+
}
|
|
415
|
+
</Flex>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
<Description field={field} style={{ fontSize: 14, color: '#00000099', marginTop: 4 }} />
|
|
419
|
+
|
|
420
|
+
{field.type !== 'Question Group' &&
|
|
421
|
+
<Typography color="error" style={{ marginTop: 3, height: 10, fontSize: 14, marginBottom: -10 }}>
|
|
422
|
+
{(validationMessage === 'A response is required' || validationMessage === 'A value must be checked' || validationMessage === 'A file is required' || 'Enter a valid phone number' || 'Insurer is required')
|
|
423
|
+
? value.touched
|
|
424
|
+
? form_display_text_for_language(form, validationMessage)
|
|
425
|
+
: null
|
|
426
|
+
: form_display_text_for_language(form, validationMessage)
|
|
427
|
+
}
|
|
428
|
+
</Typography>
|
|
429
|
+
}
|
|
430
|
+
</Flex>
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export const TellescopeSingleQuestionFlowV2: typeof TellescopeFormV2 = ({
|
|
435
|
+
form,
|
|
436
|
+
activeField,
|
|
437
|
+
currentFileValue,
|
|
438
|
+
customInputs,
|
|
439
|
+
currentValue,
|
|
440
|
+
submitErrorMessage,
|
|
441
|
+
onAddFile,
|
|
442
|
+
onFieldChange,
|
|
443
|
+
goToNextField,
|
|
444
|
+
goToPreviousField,
|
|
445
|
+
isAutoAdvancing,
|
|
446
|
+
isNextDisabled,
|
|
447
|
+
isPreviousDisabled,
|
|
448
|
+
submit,
|
|
449
|
+
showSubmit,
|
|
450
|
+
submittingStatus,
|
|
451
|
+
validateField,
|
|
452
|
+
|
|
453
|
+
thanksMessage="Your response was successfully recorded",
|
|
454
|
+
htmlThanksMessage,
|
|
455
|
+
submitted,
|
|
456
|
+
onSuccess,
|
|
457
|
+
isPreview,
|
|
458
|
+
theme,
|
|
459
|
+
|
|
460
|
+
fields,
|
|
461
|
+
responses,
|
|
462
|
+
selectedFiles,
|
|
463
|
+
inputStyle,
|
|
464
|
+
repeats,
|
|
465
|
+
setRepeats,
|
|
466
|
+
|
|
467
|
+
currentPageIndex,
|
|
468
|
+
getNumberOfRemainingPages,
|
|
469
|
+
validateCurrentField,
|
|
470
|
+
handleDatabaseSelect,
|
|
471
|
+
|
|
472
|
+
setCustomerId,
|
|
473
|
+
customization,
|
|
474
|
+
enduserId,
|
|
475
|
+
enduser,
|
|
476
|
+
formResponseId,
|
|
477
|
+
groupId,
|
|
478
|
+
groupInstance,
|
|
479
|
+
logicOptions,
|
|
480
|
+
uploadingFiles, setUploadingFiles, handleFileUpload,
|
|
481
|
+
}) => {
|
|
482
|
+
const beforeunloadHandler = React.useCallback((e: BeforeUnloadEvent) => {
|
|
483
|
+
try {
|
|
484
|
+
e.preventDefault()
|
|
485
|
+
e.returnValue = 'You have unsaved changes'
|
|
486
|
+
} catch(err) { }
|
|
487
|
+
|
|
488
|
+
return ''
|
|
489
|
+
}, [])
|
|
490
|
+
|
|
491
|
+
const [uploading, setUploading] = useState(false)
|
|
492
|
+
const [autosubmitting, setAutoSubmitting] = useState(false)
|
|
493
|
+
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
// ensure redirect question doesn't trip this alert
|
|
496
|
+
if (activeField.value.type === 'Redirect') { return }
|
|
497
|
+
|
|
498
|
+
window.addEventListener('beforeunload', beforeunloadHandler)
|
|
499
|
+
return () => { window.removeEventListener('beforeunload', beforeunloadHandler) }
|
|
500
|
+
}, [beforeunloadHandler, activeField])
|
|
501
|
+
|
|
502
|
+
const handleSubmit = useCallback(async (options?: { onFileUploadsDone?: () => void }) => {
|
|
503
|
+
if (isPreview) {
|
|
504
|
+
return onSuccess?.({} as any)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
await submit({
|
|
509
|
+
onSuccess,
|
|
510
|
+
...options,
|
|
511
|
+
onPreRedirect: () => {
|
|
512
|
+
// submission may trigger a redirect, so don't block with warning message
|
|
513
|
+
try {
|
|
514
|
+
window.removeEventListener('beforeunload', beforeunloadHandler)
|
|
515
|
+
} catch(err) {}
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
}, [isPreview, onSuccess, submit, beforeunloadHandler])
|
|
519
|
+
|
|
520
|
+
const autoSubmitRef = useRef(false)
|
|
521
|
+
useEffect(() => {
|
|
522
|
+
if (!activeField.value.options?.autoSubmit) {
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
if (autoSubmitRef.current) return
|
|
526
|
+
|
|
527
|
+
if (responses.find(r => r.fieldId === activeField.value.id && field_can_autosubmit(activeField.value) && r.answer.value)) {
|
|
528
|
+
autoSubmitRef.current = true
|
|
529
|
+
setAutoSubmitting(true)
|
|
530
|
+
handleSubmit()
|
|
531
|
+
.finally(() => setAutoSubmitting(false))
|
|
532
|
+
}
|
|
533
|
+
}, [handleSubmit, responses, activeField])
|
|
534
|
+
|
|
535
|
+
const validationMessage = validateField(activeField.value)
|
|
536
|
+
|
|
537
|
+
const handleKeyPress = useCallback((e: KeyboardEvent) => {
|
|
538
|
+
if (
|
|
539
|
+
e.key === 'Enter'
|
|
540
|
+
&& !(activeField.value.type === 'Dropdown' && activeField.value.options?.other && !activeField.value.options?.radio)
|
|
541
|
+
) {
|
|
542
|
+
if (activeField.value.type === 'stringLong') return //
|
|
543
|
+
if (activeField.value.type === 'Question Group') return // ensure enter is allowed in stringLong at end of a question group before next
|
|
544
|
+
if (isNextDisabled()) return
|
|
545
|
+
goToNextField(undefined)
|
|
546
|
+
}
|
|
547
|
+
}, [activeField, isNextDisabled, goToNextField, isPreviousDisabled, goToPreviousField])
|
|
548
|
+
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
window.addEventListener('keydown', handleKeyPress)
|
|
551
|
+
return () => { window.removeEventListener('keydown', handleKeyPress)}
|
|
552
|
+
}, [handleKeyPress])
|
|
553
|
+
|
|
554
|
+
const numRemainingPages = getNumberOfRemainingPages()
|
|
555
|
+
|
|
556
|
+
// Calculate current score if real-time scoring is enabled
|
|
557
|
+
const currentScores = useMemo(() => {
|
|
558
|
+
if (!form?.realTimeScoring || !form.scoring?.length) return null
|
|
559
|
+
|
|
560
|
+
return calculate_form_scoring({
|
|
561
|
+
response: { responses },
|
|
562
|
+
form: { scoring: form.scoring }
|
|
563
|
+
})
|
|
564
|
+
}, [form?.realTimeScoring, form?.scoring, responses])
|
|
565
|
+
|
|
566
|
+
if (!(currentValue && currentFileValue)) return <></>
|
|
567
|
+
|
|
568
|
+
// Show loading state while auto-advancing to target question
|
|
569
|
+
if (isAutoAdvancing) {
|
|
570
|
+
return (
|
|
571
|
+
<Flex column alignItems="center" style={{ minHeight: 200, justifyContent: 'center' }}>
|
|
572
|
+
<CircularProgress size={40} />
|
|
573
|
+
<Typography style={{ marginTop: 16, textAlign: 'center' }}>
|
|
574
|
+
Picking up where you left off...
|
|
575
|
+
</Typography>
|
|
576
|
+
</Flex>
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return (
|
|
581
|
+
submitted
|
|
582
|
+
? <ThanksMessage htmlThanksMessage={htmlThanksMessage} thanksMessage={thanksMessage}
|
|
583
|
+
showRestartAtEnd={customization?.showRestartAtEnd}
|
|
584
|
+
/>
|
|
585
|
+
: (
|
|
586
|
+
<Flex column flex={1} style={{ justifyContent: 'space-between' }}>
|
|
587
|
+
{/* V2: Top section - progress, logo, and content */}
|
|
588
|
+
<Flex column>
|
|
589
|
+
{/* V2: Progress bar at top */}
|
|
590
|
+
{!customization?.hideProgressBar &&
|
|
591
|
+
<Progress
|
|
592
|
+
numerator={currentPageIndex + (validateCurrentField() ? 0 : 1)}
|
|
593
|
+
denominator={currentPageIndex + 1 + numRemainingPages}
|
|
594
|
+
style={{ marginBottom: '20px' }}
|
|
595
|
+
color={customization?.primaryColor ?? theme?.themeColor ?? '#798ED0'}
|
|
596
|
+
/>
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
{/* V2: Logo below progress bar, left-aligned */}
|
|
600
|
+
{!customization?.hideLogo && (
|
|
601
|
+
theme?.logoURL
|
|
602
|
+
? (
|
|
603
|
+
<Flex alignItems="flex-start" style={{ marginBottom: '20px' }}>
|
|
604
|
+
<img src={theme.logoURL} alt={theme.name} style={{ maxHeight: customization?.logoHeight || LOGO_HEIGHT, maxWidth: 225 }} />
|
|
605
|
+
</Flex>
|
|
606
|
+
)
|
|
607
|
+
: (
|
|
608
|
+
<Typography style={{ fontSize: 22, marginBottom: '20px', textAlign: 'left', fontWeight: 600 }}>
|
|
609
|
+
{theme?.name}
|
|
610
|
+
</Typography>
|
|
611
|
+
)
|
|
612
|
+
)}
|
|
613
|
+
|
|
614
|
+
<Flex column>
|
|
615
|
+
<Flex style={inputStyle}>
|
|
616
|
+
<QuestionForField form={form} fields={fields} field={activeField.value} submit={submit}
|
|
617
|
+
enduserId={enduserId} formResponseId={formResponseId}
|
|
618
|
+
enduser={enduser} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} goToNextField={goToNextField}
|
|
619
|
+
handleDatabaseSelect={handleDatabaseSelect}
|
|
620
|
+
setCustomerId={setCustomerId}
|
|
621
|
+
repeats={repeats} onRepeatsChange={setRepeats}
|
|
622
|
+
value={currentValue} file={currentFileValue}
|
|
623
|
+
customInputs={customInputs}
|
|
624
|
+
onAddFile={onAddFile} onFieldChange={onFieldChange}
|
|
625
|
+
responses={responses} selectedFiles={selectedFiles}
|
|
626
|
+
validateField={validateField}
|
|
627
|
+
groupId={groupId} groupInstance={groupInstance}
|
|
628
|
+
logicOptions={logicOptions}
|
|
629
|
+
uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
|
|
630
|
+
handleFileUpload={handleFileUpload}
|
|
631
|
+
/>
|
|
632
|
+
</Flex>
|
|
633
|
+
</Flex>
|
|
634
|
+
</Flex>
|
|
635
|
+
|
|
636
|
+
{/* V2: Buttons aligned to bottom */}
|
|
637
|
+
<Flex flex={1} alignItems={'flex-end'} justifyContent="space-between" style={{ gap: 10, marginTop: '20px' }}>
|
|
638
|
+
{!isPreviousDisabled() && (
|
|
639
|
+
<Button variant="outlined" color="secondary" disabled={isPreviousDisabled()} onClick={goToPreviousField}
|
|
640
|
+
style={{
|
|
641
|
+
...defaultButtonStyles,
|
|
642
|
+
flex: 1,
|
|
643
|
+
}}
|
|
644
|
+
>
|
|
645
|
+
{form_display_text_for_language(form, "Previous")}
|
|
646
|
+
</Button>
|
|
647
|
+
)}
|
|
648
|
+
{uploading &&
|
|
649
|
+
<Modal open setOpen={() => undefined}>
|
|
650
|
+
<Flex style={{ }} justifyContent="center">
|
|
651
|
+
<Typography style={{ fontSize: 20, width: 250, fontWeight: 'bold', textAlign: 'center' }}>
|
|
652
|
+
Uploading files...
|
|
653
|
+
</Typography>
|
|
654
|
+
|
|
655
|
+
<CircularProgress size={75} style={{ marginTop: 10, marginBottom: 10 }} />
|
|
656
|
+
|
|
657
|
+
<Typography style={{ fontSize: 20, width: 250, fontWeight: 'bold', textAlign: 'center' }}>
|
|
658
|
+
Please stay on this page until your submission is complete!
|
|
659
|
+
</Typography>
|
|
660
|
+
</Flex>
|
|
661
|
+
</Modal>
|
|
662
|
+
}
|
|
663
|
+
{showSubmit
|
|
664
|
+
? (
|
|
665
|
+
<LoadingButton muiColor="secondary"
|
|
666
|
+
onClick={() => {
|
|
667
|
+
setUploading(!!selectedFiles.find(r => !!r.blobs?.length))
|
|
668
|
+
return handleSubmit({ onFileUploadsDone: () => setUploading(false) })
|
|
669
|
+
}}
|
|
670
|
+
disabled={!!validationMessage || currentValue.field?.options?.disableNext === true || autosubmitting}
|
|
671
|
+
submitText={form_display_text_for_language(form, "Submit")}
|
|
672
|
+
submittingText={
|
|
673
|
+
submittingStatus === 'uploading-files'
|
|
674
|
+
? 'Uploading files...'
|
|
675
|
+
: "Submitting..."
|
|
676
|
+
}
|
|
677
|
+
style={{
|
|
678
|
+
...defaultButtonStyles,
|
|
679
|
+
flex: 1,
|
|
680
|
+
}}
|
|
681
|
+
/>
|
|
682
|
+
)
|
|
683
|
+
: (
|
|
684
|
+
<Button variant="contained" disabled={isNextDisabled()} onClick={() => goToNextField(undefined)}
|
|
685
|
+
color="secondary"
|
|
686
|
+
style={{
|
|
687
|
+
...defaultButtonStyles,
|
|
688
|
+
flex: 1,
|
|
689
|
+
}}
|
|
690
|
+
>
|
|
691
|
+
{form_display_text_for_language(form, "Continue")}
|
|
692
|
+
</Button>
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
</Flex>
|
|
696
|
+
|
|
697
|
+
{/* Real-time scoring display */}
|
|
698
|
+
{currentScores && currentScores.length > 0 && (
|
|
699
|
+
<Flex style={{ marginTop: 10, marginBottom: 5, width: '100%' }}>
|
|
700
|
+
{currentScores.map((score, index) => {
|
|
701
|
+
const primaryColor = customization?.primaryColor ?? theme?.themeColor ?? '#798ED0'
|
|
702
|
+
return (
|
|
703
|
+
<Flex key={index} style={{
|
|
704
|
+
padding: '10px 14px',
|
|
705
|
+
backgroundColor: '#f8f9fa',
|
|
706
|
+
borderRadius: 8,
|
|
707
|
+
border: `1px solid ${primaryColor}20`,
|
|
708
|
+
marginRight: index < currentScores.length - 1 ? 12 : 0,
|
|
709
|
+
minWidth: 120,
|
|
710
|
+
flexDirection: 'column',
|
|
711
|
+
alignItems: 'center'
|
|
712
|
+
}}>
|
|
713
|
+
<Typography style={{
|
|
714
|
+
fontSize: 12,
|
|
715
|
+
fontWeight: 'medium',
|
|
716
|
+
textAlign: 'center',
|
|
717
|
+
lineHeight: 1.2,
|
|
718
|
+
marginBottom: 4
|
|
719
|
+
}}>
|
|
720
|
+
{score.title}
|
|
721
|
+
</Typography>
|
|
722
|
+
<Typography style={{
|
|
723
|
+
fontWeight: 'bold',
|
|
724
|
+
color: primaryColor,
|
|
725
|
+
fontSize: 18
|
|
726
|
+
}}>
|
|
727
|
+
{score.value}
|
|
728
|
+
</Typography>
|
|
729
|
+
</Flex>
|
|
730
|
+
)})}
|
|
731
|
+
</Flex>
|
|
732
|
+
)}
|
|
733
|
+
|
|
734
|
+
<Typography color="error" style={{ alignText: 'center', marginTop: 3 }}>
|
|
735
|
+
{submitErrorMessage}
|
|
736
|
+
</Typography>
|
|
737
|
+
</Flex>
|
|
738
|
+
)
|
|
739
|
+
)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export const DEFAULT_THANKS_MESSAGE = "Your response was successfully recorded";
|
|
743
|
+
export const ThanksMessage = ({
|
|
744
|
+
thanksMessage,
|
|
745
|
+
htmlThanksMessage,
|
|
746
|
+
showRestartAtEnd,
|
|
747
|
+
downloadComponent,
|
|
748
|
+
} : {
|
|
749
|
+
thanksMessage?: string,
|
|
750
|
+
htmlThanksMessage?: string,
|
|
751
|
+
showRestartAtEnd?: boolean,
|
|
752
|
+
downloadComponent?: React.ReactNode,
|
|
753
|
+
}) => (
|
|
754
|
+
<Flex column>
|
|
755
|
+
{htmlThanksMessage
|
|
756
|
+
? (
|
|
757
|
+
<div style={{ textAlign: 'center' }} dangerouslySetInnerHTML={{
|
|
758
|
+
__html: remove_script_tags(htmlThanksMessage)
|
|
759
|
+
}} />
|
|
760
|
+
) : (
|
|
761
|
+
<Typography style={{ marginTop: 25, alignSelf: 'center' }}>{thanksMessage || DEFAULT_THANKS_MESSAGE}</Typography>
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
{read_local_storage('redirecting_public_group') === 'true' &&
|
|
765
|
+
<>
|
|
766
|
+
<Typography style={{ marginTop: 25, alignSelf: 'center' }}>
|
|
767
|
+
Redirecting to next form... <CircularProgress size={20} color="primary" />
|
|
768
|
+
</Typography>
|
|
769
|
+
</>
|
|
770
|
+
}
|
|
771
|
+
{downloadComponent &&
|
|
772
|
+
<Flex justifyContent="center" style={{ marginTop: 15, marginBottom: 15 }}>
|
|
773
|
+
{downloadComponent}
|
|
774
|
+
</Flex>
|
|
775
|
+
}
|
|
776
|
+
{showRestartAtEnd && window.localStorage[`ts_form_url`] &&
|
|
777
|
+
<Button variant="outlined" style={{ ...defaultButtonStyles, maxWidth: 200, marginTop: 25, alignSelf: 'center' }}
|
|
778
|
+
onClick={() => window.location.href = window.localStorage[`ts_form_url`]}
|
|
779
|
+
>
|
|
780
|
+
Submit Again
|
|
781
|
+
</Button>
|
|
782
|
+
}
|
|
783
|
+
</Flex>
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
const TellescopeFormWithContextV2: typeof TellescopeFormV2 = (props) => {
|
|
787
|
+
const theme = useOrganizationTheme()
|
|
788
|
+
|
|
789
|
+
// V2: Override MUI theme with customization colors
|
|
790
|
+
const primaryColor = props.customization?.primaryColor ?? theme?.themeColor ?? '#798ED0'
|
|
791
|
+
const secondaryColor = props.customization?.secondaryColor ?? theme?.themeColorSecondary ?? '#585E72'
|
|
792
|
+
|
|
793
|
+
return (
|
|
794
|
+
<WithTheme theme={{ primary: primaryColor, secondary: secondaryColor }}>
|
|
795
|
+
<TellescopeFormContainerV2 style={props.style} dontAddContext
|
|
796
|
+
hideBg={props.hideBg || props.form?.customization?.hideBg}
|
|
797
|
+
logoHeight={props.logoHeight}
|
|
798
|
+
backgroundColor={props.backgroundColor}
|
|
799
|
+
hideLogo={props?.customization?.hideLogo}
|
|
800
|
+
maxWidth={props.form?.customization?.maxWidth}
|
|
801
|
+
>
|
|
802
|
+
{props.submitted
|
|
803
|
+
? <ThanksMessage {...props} showRestartAtEnd={props?.customization?.showRestartAtEnd} />
|
|
804
|
+
: (<TellescopeSingleQuestionFlowV2 {...props} theme={theme} />)
|
|
805
|
+
}
|
|
806
|
+
</TellescopeFormContainerV2>
|
|
807
|
+
</WithTheme>
|
|
808
|
+
)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export const SaveDraft = ({
|
|
812
|
+
selectedFiles,
|
|
813
|
+
enduserId,
|
|
814
|
+
responses,
|
|
815
|
+
existingResponses,
|
|
816
|
+
fields,
|
|
817
|
+
onSuccess,
|
|
818
|
+
formResponseId,
|
|
819
|
+
includedFieldIds,
|
|
820
|
+
formId,
|
|
821
|
+
style,
|
|
822
|
+
disabled,
|
|
823
|
+
getResponsesWithQuestionGroupAnswers,
|
|
824
|
+
isInternalNote,
|
|
825
|
+
formTitle,
|
|
826
|
+
rootResponseId,
|
|
827
|
+
parentResponseId,
|
|
828
|
+
} : Styled & Pick<TellescopeFormProps, 'existingResponses' | 'fields' | 'onSuccess' | 'selectedFiles' | 'responses' | 'enduserId' | 'getResponsesWithQuestionGroupAnswers'> & {
|
|
829
|
+
disabled?: boolean,
|
|
830
|
+
formResponseId?: string,
|
|
831
|
+
formId: string,
|
|
832
|
+
includedFieldIds: string[]
|
|
833
|
+
isInternalNote?: boolean,
|
|
834
|
+
formTitle?: string,
|
|
835
|
+
rootResponseId?: string,
|
|
836
|
+
parentResponseId?: string,
|
|
837
|
+
}) => {
|
|
838
|
+
const [, { updateElement: updateFormResponse }] = useFormResponses({ dontFetch: true })
|
|
839
|
+
const session = useSession()
|
|
840
|
+
const { handleUpload } = useFileUpload({ })
|
|
841
|
+
|
|
842
|
+
return (
|
|
843
|
+
<LoadingButton style={style} disabled={disabled} variant='outlined'
|
|
844
|
+
onClick={async () => {
|
|
845
|
+
const hasFile = selectedFiles.find(f => !!f.blobs?.length) !== undefined
|
|
846
|
+
|
|
847
|
+
if (hasFile) {
|
|
848
|
+
try { // convert FileBlobs to FileResponses
|
|
849
|
+
for (const blobInfo of selectedFiles) {
|
|
850
|
+
const { blobs, fieldId } = blobInfo
|
|
851
|
+
if (!blobs) continue
|
|
852
|
+
|
|
853
|
+
for (const blob of blobs) {
|
|
854
|
+
const result: FormResponseAnswerFileValue = { name: blob.name, secureName: '' }
|
|
855
|
+
const { secureName } = await handleUpload(
|
|
856
|
+
{
|
|
857
|
+
name: blob.name,
|
|
858
|
+
size: blob.size,
|
|
859
|
+
type: blob.type,
|
|
860
|
+
enduserId,
|
|
861
|
+
},
|
|
862
|
+
blob
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
const responseIndex = responses.findIndex(f => f.fieldId === fieldId)
|
|
866
|
+
|
|
867
|
+
if (responses[responseIndex].answer.type === 'files') {
|
|
868
|
+
if (!responses[responseIndex].answer.value) {
|
|
869
|
+
responses[responseIndex].answer.value = []
|
|
870
|
+
}
|
|
871
|
+
(responses[responseIndex].answer.value as any[]).push({
|
|
872
|
+
...result, type: blob.type, secureName, name: result.name ?? ''
|
|
873
|
+
})
|
|
874
|
+
} else {
|
|
875
|
+
responses[responseIndex].answer.value = { ...result, type: blob.type, secureName, name: result.name ?? '' }
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
} catch(err: any) {
|
|
880
|
+
} finally {
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
const response = await updateFormResponse(
|
|
886
|
+
(
|
|
887
|
+
formResponseId
|
|
888
|
+
?? (await session.api.form_responses.prepare_form_response({ rootResponseId, parentResponseId, isInternalNote, formId, enduserId, title: formTitle })).response.id
|
|
889
|
+
),
|
|
890
|
+
{
|
|
891
|
+
draftSavedAt: new Date(),
|
|
892
|
+
draftSavedBy: session?.userInfo?.id,
|
|
893
|
+
responses: [
|
|
894
|
+
...(existingResponses ?? []).filter(r => !fields.find(f => f.id === r.fieldId)),
|
|
895
|
+
...getResponsesWithQuestionGroupAnswers(includedFieldIds.map(id => responses.find(r => r.fieldId === id)!))
|
|
896
|
+
]
|
|
897
|
+
},
|
|
898
|
+
{ replaceObjectFields: true }
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
onSuccess?.(response)
|
|
902
|
+
} catch(err: any) {
|
|
903
|
+
// setSubmitErrorMessage(err?.message ?? 'Failed to upload file')
|
|
904
|
+
} finally {
|
|
905
|
+
// setSubmittingStatus(undefined)
|
|
906
|
+
}
|
|
907
|
+
}}
|
|
908
|
+
submitText="Save Draft"
|
|
909
|
+
submittingText="Saving..."
|
|
910
|
+
/>
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export const UpdateResponse = ({
|
|
915
|
+
selectedFiles,
|
|
916
|
+
enduserId,
|
|
917
|
+
responses,
|
|
918
|
+
onSuccess,
|
|
919
|
+
formResponseId,
|
|
920
|
+
includedFieldIds,
|
|
921
|
+
formId,
|
|
922
|
+
style,
|
|
923
|
+
disabled,
|
|
924
|
+
getResponsesWithQuestionGroupAnswers,
|
|
925
|
+
existingResponses,
|
|
926
|
+
fields,
|
|
927
|
+
} : Styled & Pick<TellescopeFormProps, 'existingResponses' | 'fields' | 'onSuccess' | 'selectedFiles' | 'responses' | 'enduserId' | 'getResponsesWithQuestionGroupAnswers'> & {
|
|
928
|
+
disabled?: boolean,
|
|
929
|
+
formResponseId?: string,
|
|
930
|
+
formId: string,
|
|
931
|
+
includedFieldIds: string[]
|
|
932
|
+
}) => {
|
|
933
|
+
const [, { updateElement: updateFormResponse }] = useFormResponses({ dontFetch: true })
|
|
934
|
+
const session = useSession()
|
|
935
|
+
const { handleUpload } = useFileUpload({ })
|
|
936
|
+
|
|
937
|
+
return (
|
|
938
|
+
<LoadingButton style={style} disabled={disabled} variant='contained'
|
|
939
|
+
onClick={async () => {
|
|
940
|
+
const hasFile = selectedFiles.find(f => !!f.blobs?.length) !== undefined
|
|
941
|
+
|
|
942
|
+
if (hasFile) {
|
|
943
|
+
try { // convert FileBlobs to FileResponses
|
|
944
|
+
for (const blobInfo of selectedFiles) {
|
|
945
|
+
const { blobs, fieldId } = blobInfo
|
|
946
|
+
if (!blobs) continue
|
|
947
|
+
|
|
948
|
+
for (const blob of blobs) {
|
|
949
|
+
const result: FormResponseAnswerFileValue = { name: blob.name, secureName: '' }
|
|
950
|
+
const { secureName } = await handleUpload(
|
|
951
|
+
{
|
|
952
|
+
name: blob.name,
|
|
953
|
+
size: blob.size,
|
|
954
|
+
type: blob.type,
|
|
955
|
+
enduserId,
|
|
956
|
+
},
|
|
957
|
+
blob
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
const responseIndex = responses.findIndex(f => f.fieldId === fieldId)
|
|
961
|
+
|
|
962
|
+
if (responses[responseIndex].answer.type === 'files') {
|
|
963
|
+
if (!responses[responseIndex].answer.value) {
|
|
964
|
+
responses[responseIndex].answer.value = []
|
|
965
|
+
}
|
|
966
|
+
(responses[responseIndex].answer.value as any[]).push({
|
|
967
|
+
...result, type: blob.type, secureName, name: result.name ?? ''
|
|
968
|
+
})
|
|
969
|
+
} else {
|
|
970
|
+
responses[responseIndex].answer.value = { ...result, type: blob.type, secureName, name: result.name ?? '' }
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
} catch(err: any) {
|
|
976
|
+
} finally {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const response = await updateFormResponse(
|
|
981
|
+
(
|
|
982
|
+
formResponseId
|
|
983
|
+
?? (await session.api.form_responses.prepare_form_response({ formId, enduserId })).response.id
|
|
984
|
+
),
|
|
985
|
+
{
|
|
986
|
+
responses: [
|
|
987
|
+
...(existingResponses ?? []).filter(r => !fields.find(f => f.id === r.fieldId)),
|
|
988
|
+
...getResponsesWithQuestionGroupAnswers(includedFieldIds.map(id => responses.find(r => r.fieldId === id)!))
|
|
989
|
+
]
|
|
990
|
+
},
|
|
991
|
+
{ replaceObjectFields: true }
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
onSuccess?.(response)
|
|
995
|
+
}}
|
|
996
|
+
submitText="Update"
|
|
997
|
+
submittingText="Saving..."
|
|
998
|
+
/>
|
|
999
|
+
)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
export const Description = ({ field, color="primary", style } : { field: FormField, color?: string } & Styled) => {
|
|
1003
|
+
if (!field.htmlDescription && field.description) {
|
|
1004
|
+
return (
|
|
1005
|
+
<Typography color={color as any} style={style}>
|
|
1006
|
+
{field.description}
|
|
1007
|
+
</Typography>
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
if (!field.htmlDescription) return null
|
|
1011
|
+
|
|
1012
|
+
return (
|
|
1013
|
+
<span style={style} dangerouslySetInnerHTML={{
|
|
1014
|
+
__html: remove_script_tags(field.htmlDescription)
|
|
1015
|
+
}} />
|
|
1016
|
+
)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
export const TellescopeSinglePageForm: React.JSXElementConstructor<TellescopeFormProps & Styled & {
|
|
1020
|
+
updating?: boolean,
|
|
1021
|
+
isInternalNote?: boolean,
|
|
1022
|
+
submittedAt?: Date,
|
|
1023
|
+
updatedAt?: Date,
|
|
1024
|
+
otherEnduserIds?: string[],
|
|
1025
|
+
onBulkErrors?: (errors: { enduserId: string, message: string }[]) => void,
|
|
1026
|
+
AddToDatabase?: React.JSXElementConstructor<AddToDatabaseProps>,
|
|
1027
|
+
}> = ({
|
|
1028
|
+
customInputs,
|
|
1029
|
+
submitErrorMessage,
|
|
1030
|
+
onAddFile,
|
|
1031
|
+
onFieldChange,
|
|
1032
|
+
goToNextField,
|
|
1033
|
+
goToPreviousField,
|
|
1034
|
+
isNextDisabled,
|
|
1035
|
+
isPreviousDisabled,
|
|
1036
|
+
submit,
|
|
1037
|
+
showSubmit,
|
|
1038
|
+
showSaveDraft,
|
|
1039
|
+
submittingStatus,
|
|
1040
|
+
updating,
|
|
1041
|
+
validateField,
|
|
1042
|
+
validateResponsesForFields,
|
|
1043
|
+
formTitle,
|
|
1044
|
+
|
|
1045
|
+
thanksMessage=DEFAULT_THANKS_MESSAGE,
|
|
1046
|
+
htmlThanksMessage,
|
|
1047
|
+
submitted,
|
|
1048
|
+
style,
|
|
1049
|
+
onSuccess,
|
|
1050
|
+
isPreview,
|
|
1051
|
+
|
|
1052
|
+
fields,
|
|
1053
|
+
selectedFiles,
|
|
1054
|
+
responses,
|
|
1055
|
+
|
|
1056
|
+
isInternalNote,
|
|
1057
|
+
existingResponses,
|
|
1058
|
+
|
|
1059
|
+
repeats,
|
|
1060
|
+
setRepeats,
|
|
1061
|
+
|
|
1062
|
+
setCustomerId,
|
|
1063
|
+
|
|
1064
|
+
rootResponseId,
|
|
1065
|
+
parentResponseId,
|
|
1066
|
+
|
|
1067
|
+
handleDatabaseSelect,
|
|
1068
|
+
|
|
1069
|
+
submittedAt,
|
|
1070
|
+
updatedAt,
|
|
1071
|
+
|
|
1072
|
+
otherEnduserIds,
|
|
1073
|
+
onBulkErrors,
|
|
1074
|
+
enduser,
|
|
1075
|
+
groupId,
|
|
1076
|
+
groupInstance,
|
|
1077
|
+
uploadingFiles, setUploadingFiles, handleFileUpload,
|
|
1078
|
+
AddToDatabase,
|
|
1079
|
+
...props
|
|
1080
|
+
}) => {
|
|
1081
|
+
const list = useListForFormFields(fields, responses, { form: props.form, gender: enduser?.gender })
|
|
1082
|
+
|
|
1083
|
+
const includedFieldIds = (
|
|
1084
|
+
Array.from(new Set([
|
|
1085
|
+
...list.map(f => f.id),
|
|
1086
|
+
...(existingResponses ?? []).filter(e => !e.isPrepopulatedFromEnduserField).map(e => e.fieldId)
|
|
1087
|
+
]))
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
const handleSubmit = useCallback(async () => {
|
|
1091
|
+
if (isPreview) {
|
|
1092
|
+
return onSuccess?.({} as any)
|
|
1093
|
+
}
|
|
1094
|
+
await submit({
|
|
1095
|
+
onSuccess,
|
|
1096
|
+
includedFieldIds, /* ensures all answers are included and in the correct order */
|
|
1097
|
+
otherEnduserIds, onBulkErrors,
|
|
1098
|
+
})
|
|
1099
|
+
}, [isPreview, onSuccess, submit, otherEnduserIds, onBulkErrors])
|
|
1100
|
+
|
|
1101
|
+
const errors = useMemo(() => {
|
|
1102
|
+
const es: { id: string, title: string, error: string }[] = []
|
|
1103
|
+
|
|
1104
|
+
try {
|
|
1105
|
+
list.forEach(field => {
|
|
1106
|
+
const error = validateField(field)
|
|
1107
|
+
if (error && typeof error === 'string') es.push({
|
|
1108
|
+
id: field.id,
|
|
1109
|
+
title: field.title,
|
|
1110
|
+
error,
|
|
1111
|
+
})
|
|
1112
|
+
})
|
|
1113
|
+
} catch(err) {
|
|
1114
|
+
console.error(err)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return es
|
|
1118
|
+
}, [list, validateField])
|
|
1119
|
+
|
|
1120
|
+
let updatesDisabled = true
|
|
1121
|
+
for (const r of responses ?? []) {
|
|
1122
|
+
const match = existingResponses?.find(_r => _r.fieldId === r.fieldId)
|
|
1123
|
+
if (!match) {
|
|
1124
|
+
updatesDisabled = false
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
if (!objects_equivalent(r.answer, match.answer)) {
|
|
1128
|
+
updatesDisabled = false
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Calculate current score if real-time scoring is enabled
|
|
1134
|
+
const currentScores = useMemo(() => {
|
|
1135
|
+
if (!props.form?.realTimeScoring || !props.form.scoring?.length) return null
|
|
1136
|
+
|
|
1137
|
+
return calculate_form_scoring({
|
|
1138
|
+
response: { responses },
|
|
1139
|
+
form: { scoring: props.form.scoring }
|
|
1140
|
+
})
|
|
1141
|
+
}, [props.form?.realTimeScoring, props.form?.scoring, responses])
|
|
1142
|
+
|
|
1143
|
+
return (
|
|
1144
|
+
<Flex flex={1} column>
|
|
1145
|
+
{submitted
|
|
1146
|
+
? <ThanksMessage htmlThanksMessage={htmlThanksMessage} thanksMessage={thanksMessage}
|
|
1147
|
+
showRestartAtEnd={props?.customization?.showRestartAtEnd}
|
|
1148
|
+
/>
|
|
1149
|
+
: (
|
|
1150
|
+
<>
|
|
1151
|
+
{/* Real-time scoring display - pinned to top */}
|
|
1152
|
+
{currentScores && currentScores.length > 0 && (
|
|
1153
|
+
<Flex style={{
|
|
1154
|
+
position: 'sticky',
|
|
1155
|
+
top: 0,
|
|
1156
|
+
zIndex: 1000,
|
|
1157
|
+
backgroundColor: 'white',
|
|
1158
|
+
borderBottom: '1px solid #e0e0e0',
|
|
1159
|
+
padding: '12px 0',
|
|
1160
|
+
marginBottom: '16px',
|
|
1161
|
+
width: '100%',
|
|
1162
|
+
justifyContent: 'center'
|
|
1163
|
+
}}>
|
|
1164
|
+
{currentScores.map((score, index) => (
|
|
1165
|
+
<Flex key={index} style={{
|
|
1166
|
+
padding: '10px 14px',
|
|
1167
|
+
backgroundColor: '#f8f9fa',
|
|
1168
|
+
borderRadius: 8,
|
|
1169
|
+
border: `1px solid ${PRIMARY_HEX}20`,
|
|
1170
|
+
marginRight: index < currentScores.length - 1 ? 12 : 0,
|
|
1171
|
+
minWidth: 120,
|
|
1172
|
+
flexDirection: 'column',
|
|
1173
|
+
alignItems: 'center'
|
|
1174
|
+
}}>
|
|
1175
|
+
<Typography style={{
|
|
1176
|
+
fontSize: 12,
|
|
1177
|
+
fontWeight: 'medium',
|
|
1178
|
+
textAlign: 'center',
|
|
1179
|
+
lineHeight: 1.2,
|
|
1180
|
+
marginBottom: 4
|
|
1181
|
+
}}>
|
|
1182
|
+
{score.title}
|
|
1183
|
+
</Typography>
|
|
1184
|
+
<Typography style={{
|
|
1185
|
+
fontWeight: 'bold',
|
|
1186
|
+
color: PRIMARY_HEX,
|
|
1187
|
+
fontSize: 18
|
|
1188
|
+
}}>
|
|
1189
|
+
{score.value}
|
|
1190
|
+
</Typography>
|
|
1191
|
+
</Flex>
|
|
1192
|
+
))}
|
|
1193
|
+
</Flex>
|
|
1194
|
+
)}
|
|
1195
|
+
|
|
1196
|
+
<Flex flex={1} justifyContent={"center"} column style={{ marginBottom: 15 }}>
|
|
1197
|
+
{list.map((activeField) => {
|
|
1198
|
+
const value = responses.find(r => r.fieldId === activeField.id)!
|
|
1199
|
+
const file = selectedFiles.find(r => r.fieldId === activeField.id)!
|
|
1200
|
+
|
|
1201
|
+
return (
|
|
1202
|
+
<Flex key={activeField.id} style={{ marginBottom: 5 }}>
|
|
1203
|
+
<Flex column flex={1}>
|
|
1204
|
+
<QuestionForField isSinglePage fields={fields} field={activeField} handleDatabaseSelect={handleDatabaseSelect}
|
|
1205
|
+
enduserId={props.enduserId} formResponseId={props.formResponseId} rootResponseId={rootResponseId} submit={submit}
|
|
1206
|
+
enduser={enduser} goToPreviousField={goToPreviousField} isPreviousDisabled={isPreviousDisabled} goToNextField={goToNextField}
|
|
1207
|
+
repeats={repeats} onRepeatsChange={setRepeats} setCustomerId={setCustomerId}
|
|
1208
|
+
value={value} file={file}
|
|
1209
|
+
customInputs={customInputs}
|
|
1210
|
+
onAddFile={onAddFile} onFieldChange={onFieldChange}
|
|
1211
|
+
responses={responses} selectedFiles={selectedFiles}
|
|
1212
|
+
validateField={validateField}
|
|
1213
|
+
groupId={groupId} groupInstance={groupInstance}
|
|
1214
|
+
uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
|
|
1215
|
+
handleFileUpload={handleFileUpload}
|
|
1216
|
+
AddToDatabase={AddToDatabase}
|
|
1217
|
+
/>
|
|
1218
|
+
</Flex>
|
|
1219
|
+
</Flex>
|
|
1220
|
+
)
|
|
1221
|
+
})}
|
|
1222
|
+
</Flex>
|
|
1223
|
+
|
|
1224
|
+
<Flex flex={1} wrap="nowrap">
|
|
1225
|
+
{updating
|
|
1226
|
+
? (
|
|
1227
|
+
<Flex flex={1} column>
|
|
1228
|
+
<UpdateResponse
|
|
1229
|
+
{...props} fields={fields} existingResponses={existingResponses}
|
|
1230
|
+
includedFieldIds={includedFieldIds}
|
|
1231
|
+
// style={{ width: 200, marginRight: 5, height: 42 }}
|
|
1232
|
+
formId={fields[0].formId}
|
|
1233
|
+
responses={responses}
|
|
1234
|
+
selectedFiles={selectedFiles}
|
|
1235
|
+
onSuccess={onSuccess}
|
|
1236
|
+
disabled={updatesDisabled}
|
|
1237
|
+
/>
|
|
1238
|
+
|
|
1239
|
+
{submittedAt &&
|
|
1240
|
+
<Typography style={{ marginTop: 5 }}>
|
|
1241
|
+
Originally Submitted: {formatted_date(new Date(submittedAt))}
|
|
1242
|
+
</Typography>
|
|
1243
|
+
}
|
|
1244
|
+
{updatedAt &&
|
|
1245
|
+
<Typography>
|
|
1246
|
+
Last Updated: {formatted_date(new Date(updatedAt))}
|
|
1247
|
+
</Typography>
|
|
1248
|
+
}
|
|
1249
|
+
</Flex>
|
|
1250
|
+
) : (
|
|
1251
|
+
<>
|
|
1252
|
+
{showSaveDraft &&
|
|
1253
|
+
<SaveDraft existingResponses={existingResponses} fields={fields}
|
|
1254
|
+
{...props} formTitle={formTitle} isInternalNote={isInternalNote}
|
|
1255
|
+
includedFieldIds={includedFieldIds}
|
|
1256
|
+
style={{ width: 200, marginRight: 5, height: 42 }}
|
|
1257
|
+
formId={fields[0].formId}
|
|
1258
|
+
responses={responses}
|
|
1259
|
+
selectedFiles={selectedFiles}
|
|
1260
|
+
onSuccess={onSuccess}
|
|
1261
|
+
rootResponseId={rootResponseId} parentResponseId={parentResponseId}
|
|
1262
|
+
/>
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
<LoadingButton onClick={handleSubmit}
|
|
1266
|
+
disabled={!!validateResponsesForFields(list)}
|
|
1267
|
+
style={{ height: 42, width: '100%' }}
|
|
1268
|
+
submitText="Submit Response"
|
|
1269
|
+
submittingText={
|
|
1270
|
+
submittingStatus === 'uploading-files'
|
|
1271
|
+
? 'Uploading files...'
|
|
1272
|
+
: "Submitting..."
|
|
1273
|
+
}
|
|
1274
|
+
/>
|
|
1275
|
+
</>
|
|
1276
|
+
)
|
|
1277
|
+
}
|
|
1278
|
+
</Flex>
|
|
1279
|
+
|
|
1280
|
+
<Typography color="error" style={{ alignText: 'center', marginTop: 3 }}>
|
|
1281
|
+
{submitErrorMessage}
|
|
1282
|
+
</Typography>
|
|
1283
|
+
|
|
1284
|
+
{errors.length > 0 &&
|
|
1285
|
+
<>
|
|
1286
|
+
<Divider flexItem sx={{ my: 1 }} />
|
|
1287
|
+
|
|
1288
|
+
<Flex alignItems="center" wrap="nowrap">
|
|
1289
|
+
<Typography noWrap style={{ width: 200 }}>
|
|
1290
|
+
Question
|
|
1291
|
+
</Typography>
|
|
1292
|
+
<Typography noWrap style={{ }}>
|
|
1293
|
+
Error
|
|
1294
|
+
</Typography>
|
|
1295
|
+
</Flex>
|
|
1296
|
+
</>
|
|
1297
|
+
}
|
|
1298
|
+
{errors.map(e => (
|
|
1299
|
+
<Flex key={e.id} alignItems="center" wrap="nowrap">
|
|
1300
|
+
<Typography noWrap style={{ width: 200, textDecoration: 'underline', cursor: 'pointer' }}
|
|
1301
|
+
onClick={() => {
|
|
1302
|
+
try {
|
|
1303
|
+
document.getElementById(e.id)?.scrollIntoView({ behavior: 'smooth' });
|
|
1304
|
+
} catch(err) {
|
|
1305
|
+
console.error(err)
|
|
1306
|
+
}
|
|
1307
|
+
}}
|
|
1308
|
+
>
|
|
1309
|
+
{truncate_string(e.title, { length: 50 })}
|
|
1310
|
+
</Typography>
|
|
1311
|
+
|
|
1312
|
+
<Typography color="error" style={{ width: 300 }}>
|
|
1313
|
+
{e.error}
|
|
1314
|
+
</Typography>
|
|
1315
|
+
</Flex>
|
|
1316
|
+
))}
|
|
1317
|
+
</>
|
|
1318
|
+
)}
|
|
1319
|
+
</Flex>
|
|
1320
|
+
)
|
|
1321
|
+
}
|