@tellescope/react-components 1.224.0 → 1.226.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/CMS/ContentViewer.d.ts.map +1 -1
- package/lib/cjs/CMS/ContentViewer.js +28 -11
- package/lib/cjs/CMS/ContentViewer.js.map +1 -1
- package/lib/cjs/Forms/form_responses.d.ts.map +1 -1
- package/lib/cjs/Forms/form_responses.js +2 -2
- package/lib/cjs/Forms/form_responses.js.map +1 -1
- package/lib/cjs/Forms/forms.d.ts.map +1 -1
- package/lib/cjs/Forms/forms.js +81 -9
- package/lib/cjs/Forms/forms.js.map +1 -1
- package/lib/cjs/Forms/hooks.d.ts +6 -1
- package/lib/cjs/Forms/hooks.d.ts.map +1 -1
- package/lib/cjs/Forms/hooks.js +66 -12
- package/lib/cjs/Forms/hooks.js.map +1 -1
- package/lib/cjs/Forms/inputs.d.ts +1 -1
- package/lib/cjs/Forms/inputs.js +3 -3
- package/lib/cjs/Forms/inputs.js.map +1 -1
- package/lib/cjs/state.d.ts.map +1 -1
- package/lib/cjs/state.js +50 -25
- package/lib/cjs/state.js.map +1 -1
- package/lib/esm/CMS/ContentViewer.d.ts.map +1 -1
- package/lib/esm/CMS/ContentViewer.js +28 -11
- package/lib/esm/CMS/ContentViewer.js.map +1 -1
- package/lib/esm/Forms/form_responses.d.ts.map +1 -1
- package/lib/esm/Forms/form_responses.js +2 -2
- package/lib/esm/Forms/form_responses.js.map +1 -1
- package/lib/esm/Forms/forms.d.ts.map +1 -1
- package/lib/esm/Forms/forms.js +82 -10
- package/lib/esm/Forms/forms.js.map +1 -1
- package/lib/esm/Forms/hooks.d.ts +6 -1
- package/lib/esm/Forms/hooks.d.ts.map +1 -1
- package/lib/esm/Forms/hooks.js +66 -12
- package/lib/esm/Forms/hooks.js.map +1 -1
- package/lib/esm/Forms/inputs.d.ts +1 -1
- package/lib/esm/Forms/inputs.js +3 -3
- package/lib/esm/Forms/inputs.js.map +1 -1
- package/lib/esm/state.d.ts.map +1 -1
- package/lib/esm/state.js +50 -25
- package/lib/esm/state.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/CMS/ContentViewer.tsx +38 -10
- package/src/Forms/form_responses.tsx +16 -14
- package/src/Forms/forms.tsx +129 -15
- package/src/Forms/hooks.tsx +77 -9
- package/src/Forms/inputs.tsx +3 -3
- package/src/state.tsx +57 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.226.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./lib/cjs/index.js",
|
|
6
6
|
"module": "./lib/esm/index.js",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"@reduxjs/toolkit": "^1.6.2",
|
|
48
48
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
49
49
|
"@stripe/stripe-js": "^1.52.1",
|
|
50
|
-
"@tellescope/constants": "^1.
|
|
51
|
-
"@tellescope/sdk": "^1.
|
|
52
|
-
"@tellescope/types-client": "^1.
|
|
53
|
-
"@tellescope/types-models": "^1.
|
|
54
|
-
"@tellescope/types-utilities": "^1.
|
|
55
|
-
"@tellescope/utilities": "^1.
|
|
56
|
-
"@tellescope/validation": "^1.
|
|
50
|
+
"@tellescope/constants": "^1.226.0",
|
|
51
|
+
"@tellescope/sdk": "^1.226.0",
|
|
52
|
+
"@tellescope/types-client": "^1.226.0",
|
|
53
|
+
"@tellescope/types-models": "^1.226.0",
|
|
54
|
+
"@tellescope/types-utilities": "^1.226.0",
|
|
55
|
+
"@tellescope/utilities": "^1.226.0",
|
|
56
|
+
"@tellescope/validation": "^1.226.0",
|
|
57
57
|
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
|
58
58
|
"@typescript-eslint/parser": "^4.33.0",
|
|
59
59
|
"css-to-react-native": "^3.0.0",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
84
84
|
"react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
|
|
85
85
|
},
|
|
86
|
-
"gitHead": "
|
|
86
|
+
"gitHead": "49646dd7f5488090911a4ffeddb5d771603de295",
|
|
87
87
|
"publishConfig": {
|
|
88
88
|
"access": "public"
|
|
89
89
|
}
|
|
@@ -117,6 +117,28 @@ export const correct_youtube_link_for_embed = (link: string) => {
|
|
|
117
117
|
return link.replace('/watch?v=', '/embed/').split('&')[0]
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
const blockStyleToCSS = (style?: any): React.CSSProperties => {
|
|
121
|
+
if (!style) return {}
|
|
122
|
+
|
|
123
|
+
const cssStyle: React.CSSProperties = {}
|
|
124
|
+
|
|
125
|
+
if (style.width) cssStyle.width = `${style.width}px`
|
|
126
|
+
if (style.height) cssStyle.height = `${style.height}px`
|
|
127
|
+
if (style.backgroundColor) cssStyle.backgroundColor = style.backgroundColor
|
|
128
|
+
if (style.textColor) cssStyle.color = style.textColor
|
|
129
|
+
if (style.borderColor || style.borderWidth) {
|
|
130
|
+
cssStyle.border = `${style.borderWidth || 1}px solid ${style.borderColor || '#cccccc'}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add default styling when any box style is applied
|
|
134
|
+
if (Object.keys(cssStyle).length > 0) {
|
|
135
|
+
cssStyle.padding = cssStyle.padding || '10px'
|
|
136
|
+
cssStyle.display = cssStyle.display || 'inline-block'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return cssStyle
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
export const ArticleViewer = ({
|
|
121
143
|
article,
|
|
122
144
|
width,
|
|
@@ -186,13 +208,17 @@ export const ArticleViewer = ({
|
|
|
186
208
|
<Grid item key={i}>
|
|
187
209
|
{
|
|
188
210
|
block.type === 'h1' ? (
|
|
189
|
-
<Typography component="h1" sx={{ fontSize: 28, fontWeight: 'bold', m: 0, p: 0 }}>{block.info.text}</Typography>
|
|
211
|
+
<Typography component="h1" sx={{ fontSize: 28, fontWeight: 'bold', m: 0, p: 0 }} style={blockStyleToCSS(block.style)}>{block.info.text}</Typography>
|
|
190
212
|
)
|
|
191
213
|
: block.type === 'h2' ? (
|
|
192
|
-
<Typography component="h2" sx={{ fontSize: 23, m: 0, p: 0 }}>{block.info.text}</Typography>
|
|
214
|
+
<Typography component="h2" sx={{ fontSize: 23, m: 0, p: 0 }} style={blockStyleToCSS(block.style)}>{block.info.text}</Typography>
|
|
193
215
|
)
|
|
194
216
|
: block.type === 'html' ? (
|
|
195
|
-
<div style={{
|
|
217
|
+
<div style={{
|
|
218
|
+
fontSize: 18,
|
|
219
|
+
lineHeight: '25pt',
|
|
220
|
+
...blockStyleToCSS(block.style)
|
|
221
|
+
}}
|
|
196
222
|
className={css`p {
|
|
197
223
|
margin-top: 0;
|
|
198
224
|
margin-bottom: 0;
|
|
@@ -201,15 +227,16 @@ export const ArticleViewer = ({
|
|
|
201
227
|
__html: remove_script_tags(
|
|
202
228
|
block.info.html.replaceAll(/style="*"/g, '')
|
|
203
229
|
)
|
|
204
|
-
}}
|
|
230
|
+
}}
|
|
205
231
|
/>
|
|
206
232
|
)
|
|
207
233
|
: block.type === 'image' ? (
|
|
208
|
-
<img src={block.info.link} alt={''} style={{
|
|
209
|
-
maxWidth: block.info.maxWidth || '100%',
|
|
234
|
+
<img src={block.info.link} alt={block.info.alt || ''} style={{
|
|
235
|
+
maxWidth: block.info.maxWidth || '100%',
|
|
210
236
|
maxHeight: block.info.maxHeight || undefined, // '' => undefined
|
|
211
237
|
height: block.info.height || undefined, // '' => undefined
|
|
212
238
|
width: block.info.width || undefined, // '' => undefined
|
|
239
|
+
...blockStyleToCSS(block.style)
|
|
213
240
|
}} />
|
|
214
241
|
)
|
|
215
242
|
: block.type === 'youtube' ? (
|
|
@@ -254,12 +281,13 @@ export const ArticleViewer = ({
|
|
|
254
281
|
)
|
|
255
282
|
}
|
|
256
283
|
|
|
284
|
+
|
|
257
285
|
export const html_for_article = (article: ManagedContentRecord, options?: { rootWidth?: number }) => {
|
|
258
286
|
const rootWidth = options?.rootWidth || 400
|
|
259
287
|
|
|
260
288
|
const content = (
|
|
261
289
|
(article.blocks ?? [])
|
|
262
|
-
.map((block, i) =>
|
|
290
|
+
.map((block, i) =>
|
|
263
291
|
block.type === 'h1' ? (
|
|
264
292
|
`<h1>${block.info.text}</h1>`
|
|
265
293
|
)
|
|
@@ -268,11 +296,11 @@ export const html_for_article = (article: ManagedContentRecord, options?: { root
|
|
|
268
296
|
)
|
|
269
297
|
: block.type === 'html' ? (
|
|
270
298
|
`<div>${remove_script_tags(remove_script_tags(block.info.html))}</div>`
|
|
271
|
-
)
|
|
299
|
+
)
|
|
272
300
|
: block.type === 'image' ? (
|
|
273
|
-
// wrap with div to supporting centering later
|
|
301
|
+
// wrap with div to supporting centering later
|
|
274
302
|
`<div style="">
|
|
275
|
-
<img src="${block.info.link}" alt={''} style="max-width: ${block.info.maxWidth || '100%'}; max-height: ${block.info.maxHeight || undefined}; height: ${block.info.height || undefined}; width: ${block.info.width || undefined};" />
|
|
303
|
+
<img src="${block.info.link}" alt="${block.info.alt || ''}" style="max-width: ${block.info.maxWidth || '100%'}; max-height: ${block.info.maxHeight || undefined}; height: ${block.info.height || undefined}; width: ${block.info.width || undefined};" />
|
|
276
304
|
</div>`
|
|
277
305
|
)
|
|
278
306
|
: block.type === 'youtube' ? (
|
|
@@ -23,8 +23,8 @@ export const AddressDisplay = ({ value } : { value: Required<FormResponseAnswerA
|
|
|
23
23
|
</Grid>
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
export const ResponseAnswer = ({ formResponse, fieldId, isHTML, answer: a, printing, onImageClick } : {
|
|
27
|
-
answer: FormResponseValueAnswer,
|
|
26
|
+
export const ResponseAnswer = ({ formResponse, fieldId, isHTML, answer: a, printing, onImageClick } : {
|
|
27
|
+
answer: FormResponseValueAnswer,
|
|
28
28
|
formResponse: FormResponse,
|
|
29
29
|
fieldId: string,
|
|
30
30
|
printing?: boolean,
|
|
@@ -57,9 +57,9 @@ export const ResponseAnswer = ({ formResponse, fieldId, isHTML, answer: a, print
|
|
|
57
57
|
)}
|
|
58
58
|
</ol>
|
|
59
59
|
: a.type === 'file'
|
|
60
|
-
? a.value.secureName
|
|
60
|
+
? a.value.secureName
|
|
61
61
|
? <Typography>
|
|
62
|
-
{!printing
|
|
62
|
+
{!printing
|
|
63
63
|
? <DownloadFileIconButton secureName={a.value.secureName} onDownload={url => window.open(url, '_blank')} />
|
|
64
64
|
: (
|
|
65
65
|
<SecureImage secureName={a.value.secureName} onImageClick={onImageClick}
|
|
@@ -74,10 +74,10 @@ export const ResponseAnswer = ({ formResponse, fieldId, isHTML, answer: a, print
|
|
|
74
74
|
: a.type === 'files'
|
|
75
75
|
? a.value.map(file => (
|
|
76
76
|
<Typography key={file.secureName}>
|
|
77
|
-
{!printing
|
|
77
|
+
{!printing
|
|
78
78
|
? <DownloadFileIconButton secureName={file.secureName} onDownload={url => window.open(url, '_blank')} />
|
|
79
79
|
: (
|
|
80
|
-
<SecureImage secureName={file.secureName}
|
|
80
|
+
<SecureImage secureName={file.secureName}
|
|
81
81
|
style={{ maxHeight: 400, maxWidth: 400 }}
|
|
82
82
|
/>
|
|
83
83
|
)
|
|
@@ -308,14 +308,14 @@ export const FormResponseView = ({ showAnswerInline=true, logoURL, enduser, onCl
|
|
|
308
308
|
|
|
309
309
|
<div style={{ flexDirection: "column", display: 'flex', flex: 1 }}>
|
|
310
310
|
{response.responses.map((r, i) => (
|
|
311
|
-
<div key={i} style={{ marginBottom:
|
|
311
|
+
<div key={i} style={{ marginBottom: 36 }}>
|
|
312
312
|
<div style={{ display: 'flex', flex: 1, flexDirection: "row", justifyContent: 'space-between', flexWrap: 'nowrap' }}>
|
|
313
|
-
{r.fieldTitle &&
|
|
313
|
+
{r.fieldTitle &&
|
|
314
314
|
<div style={{ }}>
|
|
315
315
|
<Typography style={{
|
|
316
316
|
fontWeight: 'bold',
|
|
317
317
|
width: (
|
|
318
|
-
showAnswerInline
|
|
318
|
+
showAnswerInline
|
|
319
319
|
? '400px'
|
|
320
320
|
: undefined
|
|
321
321
|
)
|
|
@@ -326,10 +326,10 @@ export const FormResponseView = ({ showAnswerInline=true, logoURL, enduser, onCl
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
<div style={{ }}>
|
|
329
|
-
{showAnswerInline && r.answer.type !== 'Question Group'
|
|
329
|
+
{showAnswerInline && r.answer.type !== 'Question Group'
|
|
330
330
|
&& !(typeof r.answer.value === 'string' && r.answer.value.includes('{TELLESCOPE')) // hidden field for matching, not to display
|
|
331
331
|
&& (
|
|
332
|
-
(r.answerIsHTML && typeof r.answer.value === 'string')
|
|
332
|
+
(r.answerIsHTML && typeof r.answer.value === 'string')
|
|
333
333
|
? <div dangerouslySetInnerHTML={{ __html: remove_script_tags(r.answer.value) }} />
|
|
334
334
|
: <ResponseAnswer fieldId={r.fieldId} formResponse={response} answer={r.answer} printing={printing} />
|
|
335
335
|
)
|
|
@@ -339,7 +339,7 @@ export const FormResponseView = ({ showAnswerInline=true, logoURL, enduser, onCl
|
|
|
339
339
|
|
|
340
340
|
{r.fieldDescription
|
|
341
341
|
? (
|
|
342
|
-
<Typography style={{
|
|
342
|
+
<Typography style={{ }}>
|
|
343
343
|
{r.fieldDescription}
|
|
344
344
|
</Typography>
|
|
345
345
|
): r.fieldHtmlDescription
|
|
@@ -349,10 +349,12 @@ export const FormResponseView = ({ showAnswerInline=true, logoURL, enduser, onCl
|
|
|
349
349
|
}} />
|
|
350
350
|
)
|
|
351
351
|
: null
|
|
352
|
-
}
|
|
352
|
+
}
|
|
353
353
|
|
|
354
354
|
{!showAnswerInline &&
|
|
355
|
-
<
|
|
355
|
+
<div style={{ }}>
|
|
356
|
+
<ResponseAnswer answer={r.answer} formResponse={response} fieldId={r.fieldId} />
|
|
357
|
+
</div>
|
|
356
358
|
}
|
|
357
359
|
</div>
|
|
358
360
|
)
|
package/src/Forms/forms.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { AddToDatabaseProps, AddressInput, AllergiesInput, AppointmentBookingInp
|
|
|
6
6
|
import { PRIMARY_HEX } from "@tellescope/constants"
|
|
7
7
|
import { FormResponse, FormField, Form, Enduser } from "@tellescope/types-client"
|
|
8
8
|
import { FormResponseAnswerFileValue, OrganizationTheme } from "@tellescope/types-models"
|
|
9
|
-
import { 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"
|
|
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
10
|
import { Divider } from "@mui/material"
|
|
11
11
|
|
|
12
12
|
export const TellescopeFormContainer = ({ businessId, organizationIds, ...props } : {
|
|
@@ -315,7 +315,7 @@ export const QuestionForField = ({
|
|
|
315
315
|
<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} />
|
|
316
316
|
)
|
|
317
317
|
: field.type === 'Stripe' ? (
|
|
318
|
-
<Stripe field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} />
|
|
318
|
+
<Stripe enduserId={enduserId} field={field} value={value.answer.value as string} onChange={onFieldChange as ChangeHandler<any>} setCustomerId={setCustomerId} form={form} />
|
|
319
319
|
)
|
|
320
320
|
: field.type === 'Chargebee' ? (
|
|
321
321
|
<Chargebee field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Chargebee'>} setCustomerId={setCustomerId} form={form} />
|
|
@@ -455,15 +455,16 @@ export const QuestionForField = ({
|
|
|
455
455
|
|
|
456
456
|
export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
|
|
457
457
|
form,
|
|
458
|
-
activeField,
|
|
458
|
+
activeField,
|
|
459
459
|
currentFileValue,
|
|
460
|
-
customInputs,
|
|
461
|
-
currentValue,
|
|
460
|
+
customInputs,
|
|
461
|
+
currentValue,
|
|
462
462
|
submitErrorMessage,
|
|
463
463
|
onAddFile,
|
|
464
|
-
onFieldChange,
|
|
464
|
+
onFieldChange,
|
|
465
465
|
goToNextField,
|
|
466
466
|
goToPreviousField,
|
|
467
|
+
isAutoAdvancing,
|
|
467
468
|
isNextDisabled,
|
|
468
469
|
isPreviousDisabled,
|
|
469
470
|
submit,
|
|
@@ -574,7 +575,30 @@ export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
|
|
|
574
575
|
|
|
575
576
|
const numRemainingPages = getNumberOfRemainingPages()
|
|
576
577
|
|
|
578
|
+
// Calculate current score if real-time scoring is enabled
|
|
579
|
+
const currentScores = useMemo(() => {
|
|
580
|
+
if (!form?.realTimeScoring || !form.scoring?.length) return null
|
|
581
|
+
|
|
582
|
+
return calculate_form_scoring({
|
|
583
|
+
response: { responses },
|
|
584
|
+
form: { scoring: form.scoring }
|
|
585
|
+
})
|
|
586
|
+
}, [form?.realTimeScoring, form?.scoring, responses])
|
|
587
|
+
|
|
577
588
|
if (!(currentValue && currentFileValue)) return <></>
|
|
589
|
+
|
|
590
|
+
// Show loading state while auto-advancing to target question
|
|
591
|
+
if (isAutoAdvancing) {
|
|
592
|
+
return (
|
|
593
|
+
<Flex column alignItems="center" style={{ minHeight: 200, justifyContent: 'center' }}>
|
|
594
|
+
<CircularProgress size={40} />
|
|
595
|
+
<Typography style={{ marginTop: 16, textAlign: 'center' }}>
|
|
596
|
+
Picking up where you left off...
|
|
597
|
+
</Typography>
|
|
598
|
+
</Flex>
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
|
|
578
602
|
return (
|
|
579
603
|
submitted
|
|
580
604
|
? <ThanksMessage htmlThanksMessage={htmlThanksMessage} thanksMessage={thanksMessage}
|
|
@@ -590,7 +614,7 @@ export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
|
|
|
590
614
|
handleDatabaseSelect={handleDatabaseSelect}
|
|
591
615
|
setCustomerId={setCustomerId}
|
|
592
616
|
repeats={repeats} onRepeatsChange={setRepeats}
|
|
593
|
-
value={currentValue} file={currentFileValue}
|
|
617
|
+
value={currentValue} file={currentFileValue}
|
|
594
618
|
customInputs={customInputs}
|
|
595
619
|
onAddFile={onAddFile} onFieldChange={onFieldChange}
|
|
596
620
|
responses={responses} selectedFiles={selectedFiles}
|
|
@@ -658,14 +682,49 @@ export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
|
|
|
658
682
|
}
|
|
659
683
|
</Flex>
|
|
660
684
|
|
|
661
|
-
{!customization?.hideProgressBar &&
|
|
662
|
-
<Progress
|
|
663
|
-
numerator={currentPageIndex + (validateCurrentField() ? 0 : 1)}
|
|
664
|
-
denominator={currentPageIndex + 1 + numRemainingPages}
|
|
685
|
+
{!customization?.hideProgressBar &&
|
|
686
|
+
<Progress
|
|
687
|
+
numerator={currentPageIndex + (validateCurrentField() ? 0 : 1)}
|
|
688
|
+
denominator={currentPageIndex + 1 + numRemainingPages}
|
|
665
689
|
style={{ marginTop: '15px' }}
|
|
666
690
|
/>
|
|
667
691
|
}
|
|
668
692
|
|
|
693
|
+
{/* Real-time scoring display */}
|
|
694
|
+
{currentScores && currentScores.length > 0 && (
|
|
695
|
+
<Flex style={{ marginTop: 10, marginBottom: 5, width: '100%' }}>
|
|
696
|
+
{currentScores.map((score, index) => (
|
|
697
|
+
<Flex key={index} style={{
|
|
698
|
+
padding: '10px 14px',
|
|
699
|
+
backgroundColor: '#f8f9fa',
|
|
700
|
+
borderRadius: 8,
|
|
701
|
+
border: `1px solid ${theme?.themeColor || PRIMARY_HEX}20`,
|
|
702
|
+
marginRight: index < currentScores.length - 1 ? 12 : 0,
|
|
703
|
+
minWidth: 120,
|
|
704
|
+
flexDirection: 'column',
|
|
705
|
+
alignItems: 'center'
|
|
706
|
+
}}>
|
|
707
|
+
<Typography style={{
|
|
708
|
+
fontSize: 12,
|
|
709
|
+
fontWeight: 'medium',
|
|
710
|
+
textAlign: 'center',
|
|
711
|
+
lineHeight: 1.2,
|
|
712
|
+
marginBottom: 4
|
|
713
|
+
}}>
|
|
714
|
+
{score.title}
|
|
715
|
+
</Typography>
|
|
716
|
+
<Typography style={{
|
|
717
|
+
fontWeight: 'bold',
|
|
718
|
+
color: theme?.themeColor || PRIMARY_HEX,
|
|
719
|
+
fontSize: 18
|
|
720
|
+
}}>
|
|
721
|
+
{score.value}
|
|
722
|
+
</Typography>
|
|
723
|
+
</Flex>
|
|
724
|
+
))}
|
|
725
|
+
</Flex>
|
|
726
|
+
)}
|
|
727
|
+
|
|
669
728
|
<Typography color="error" style={{ alignText: 'center', marginTop: 3 }}>
|
|
670
729
|
{submitErrorMessage}
|
|
671
730
|
</Typography>
|
|
@@ -1059,16 +1118,71 @@ export const TellescopeSinglePageForm: React.JSXElementConstructor<TellescopeFor
|
|
|
1059
1118
|
}
|
|
1060
1119
|
}
|
|
1061
1120
|
|
|
1121
|
+
// Calculate current score if real-time scoring is enabled
|
|
1122
|
+
const currentScores = useMemo(() => {
|
|
1123
|
+
if (!props.form?.realTimeScoring || !props.form.scoring?.length) return null
|
|
1124
|
+
|
|
1125
|
+
return calculate_form_scoring({
|
|
1126
|
+
response: { responses },
|
|
1127
|
+
form: { scoring: props.form.scoring }
|
|
1128
|
+
})
|
|
1129
|
+
}, [props.form?.realTimeScoring, props.form?.scoring, responses])
|
|
1130
|
+
|
|
1062
1131
|
return (
|
|
1063
1132
|
<Flex flex={1} column>
|
|
1064
|
-
{submitted
|
|
1065
|
-
? <ThanksMessage htmlThanksMessage={htmlThanksMessage} thanksMessage={thanksMessage}
|
|
1066
|
-
showRestartAtEnd={props?.customization?.showRestartAtEnd}
|
|
1133
|
+
{submitted
|
|
1134
|
+
? <ThanksMessage htmlThanksMessage={htmlThanksMessage} thanksMessage={thanksMessage}
|
|
1135
|
+
showRestartAtEnd={props?.customization?.showRestartAtEnd}
|
|
1067
1136
|
/>
|
|
1068
1137
|
: (
|
|
1069
1138
|
<>
|
|
1139
|
+
{/* Real-time scoring display - pinned to top */}
|
|
1140
|
+
{currentScores && currentScores.length > 0 && (
|
|
1141
|
+
<Flex style={{
|
|
1142
|
+
position: 'sticky',
|
|
1143
|
+
top: 0,
|
|
1144
|
+
zIndex: 1000,
|
|
1145
|
+
backgroundColor: 'white',
|
|
1146
|
+
borderBottom: '1px solid #e0e0e0',
|
|
1147
|
+
padding: '12px 0',
|
|
1148
|
+
marginBottom: '16px',
|
|
1149
|
+
width: '100%',
|
|
1150
|
+
justifyContent: 'center'
|
|
1151
|
+
}}>
|
|
1152
|
+
{currentScores.map((score, index) => (
|
|
1153
|
+
<Flex key={index} style={{
|
|
1154
|
+
padding: '10px 14px',
|
|
1155
|
+
backgroundColor: '#f8f9fa',
|
|
1156
|
+
borderRadius: 8,
|
|
1157
|
+
border: `1px solid ${PRIMARY_HEX}20`,
|
|
1158
|
+
marginRight: index < currentScores.length - 1 ? 12 : 0,
|
|
1159
|
+
minWidth: 120,
|
|
1160
|
+
flexDirection: 'column',
|
|
1161
|
+
alignItems: 'center'
|
|
1162
|
+
}}>
|
|
1163
|
+
<Typography style={{
|
|
1164
|
+
fontSize: 12,
|
|
1165
|
+
fontWeight: 'medium',
|
|
1166
|
+
textAlign: 'center',
|
|
1167
|
+
lineHeight: 1.2,
|
|
1168
|
+
marginBottom: 4
|
|
1169
|
+
}}>
|
|
1170
|
+
{score.title}
|
|
1171
|
+
</Typography>
|
|
1172
|
+
<Typography style={{
|
|
1173
|
+
fontWeight: 'bold',
|
|
1174
|
+
color: PRIMARY_HEX,
|
|
1175
|
+
fontSize: 18
|
|
1176
|
+
}}>
|
|
1177
|
+
{score.value}
|
|
1178
|
+
</Typography>
|
|
1179
|
+
</Flex>
|
|
1180
|
+
))}
|
|
1181
|
+
</Flex>
|
|
1182
|
+
)}
|
|
1183
|
+
|
|
1070
1184
|
<Flex flex={1} justifyContent={"center"} column style={{ marginBottom: 15 }}>
|
|
1071
|
-
{list.map((activeField
|
|
1185
|
+
{list.map((activeField) => {
|
|
1072
1186
|
const value = responses.find(r => r.fieldId === activeField.id)!
|
|
1073
1187
|
const file = selectedFiles.find(r => r.fieldId === activeField.id)!
|
|
1074
1188
|
|
package/src/Forms/hooks.tsx
CHANGED
|
@@ -353,6 +353,7 @@ interface UseTellescopeFormOptions {
|
|
|
353
353
|
isInternalNote?: boolean,
|
|
354
354
|
formTitle?: string,
|
|
355
355
|
customization?: FormCustomization,
|
|
356
|
+
startingFieldId?: string,
|
|
356
357
|
ga4measurementId?: string,
|
|
357
358
|
submitRedirectURL?: string,
|
|
358
359
|
rootResponseId?: string,
|
|
@@ -364,6 +365,9 @@ interface UseTellescopeFormOptions {
|
|
|
364
365
|
enduser?: Partial<Enduser>,
|
|
365
366
|
isPublicForm?: boolean,
|
|
366
367
|
dontAutoadvance?: boolean,
|
|
368
|
+
groupId?: string,
|
|
369
|
+
groupInstance?: string,
|
|
370
|
+
groupPosition?: number,
|
|
367
371
|
}
|
|
368
372
|
|
|
369
373
|
const OrganizationThemeContext = createContext(null as any as {
|
|
@@ -512,7 +516,7 @@ const shouldCallout = (field: FormField | undefined, value: FormResponseValueAns
|
|
|
512
516
|
|
|
513
517
|
export type Response = FormResponseValue & { touched: boolean, includeInSubmit: boolean, field: FormField }
|
|
514
518
|
export type FileResponse = { fieldId: string, fieldTitle: string, blobs?: FileBlob[] }
|
|
515
|
-
export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogicValue, customization, carePlanId, calendarEventId, context, ga4measurementId, rootResponseId, parentResponseId, accessCode, existingResponses, automationStepId, enduserId, formResponseId, fields, isInternalNote, formTitle, submitRedirectURL,enduser }: UseTellescopeFormOptions) => {
|
|
519
|
+
export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogicValue, customization, carePlanId, calendarEventId, context, ga4measurementId, rootResponseId, parentResponseId, accessCode, existingResponses, automationStepId, enduserId, formResponseId, fields, isInternalNote, formTitle, submitRedirectURL, enduser, groupId, groupInstance, groupPosition, startingFieldId }: UseTellescopeFormOptions) => {
|
|
516
520
|
const { amPm, hoursAmPm, minutes } = get_time_values(new Date())
|
|
517
521
|
|
|
518
522
|
const root = useTreeForFormFields(fields)
|
|
@@ -528,13 +532,19 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
528
532
|
const [, { updateElement: updateFormResponse, updateLocalElement: updateLocalFormResponse }] = useFormResponses({ dontFetch: true })
|
|
529
533
|
|
|
530
534
|
const [customerId, setCustomerId] = useState<string>()
|
|
531
|
-
|
|
535
|
+
|
|
536
|
+
const [activeField, setActiveField] = useState(root)
|
|
532
537
|
const [submittingStatus, setSubmittingStatus] = useState<SubmitStatus>(undefined)
|
|
533
538
|
const [submitErrorMessage, setSubmitErrorMessage] = useState('')
|
|
534
539
|
const [currentPageIndex, setCurrentPageIndex] = useState(0)
|
|
535
|
-
const [uploadingFiles, setUploadingFiles] = useState<{ fieldId: string }[]>([])
|
|
540
|
+
const [uploadingFiles, setUploadingFiles] = useState<{ fieldId: string }[]>([])
|
|
536
541
|
const prevFieldStackRef = useRef<typeof root[]>([])
|
|
537
542
|
|
|
543
|
+
// Auto-advance state for form continuation
|
|
544
|
+
const [isAutoAdvancing, setIsAutoAdvancing] = useState(false)
|
|
545
|
+
const autoAdvanceCompletedRef = useRef(false)
|
|
546
|
+
const autoAdvanceStartTimeRef = useRef<number | null>(null)
|
|
547
|
+
|
|
538
548
|
const [repeats, setRepeats] = useState({} as Record<string, string | number>)
|
|
539
549
|
|
|
540
550
|
const gaEventRef = useRef({} as Record<string, boolean>)
|
|
@@ -674,6 +684,8 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
674
684
|
setResponses(initializeFields())
|
|
675
685
|
}, [formId, initializeFields])
|
|
676
686
|
|
|
687
|
+
|
|
688
|
+
|
|
677
689
|
// placeholders for initial files, reset when fields prop changes, since questions are now different (e.g. different form selected)
|
|
678
690
|
const fileInitRef = useRef('')
|
|
679
691
|
const initializeFiles = useCallback(() => (
|
|
@@ -701,7 +713,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
701
713
|
)
|
|
702
714
|
|
|
703
715
|
const logicOptions: NextFieldLogicOptions = {
|
|
704
|
-
urlLogicValue,
|
|
716
|
+
urlLogicValue,
|
|
705
717
|
activeResponses: responses.filter(r => r.includeInSubmit),
|
|
706
718
|
dateOfBirth: enduser?.dateOfBirth,
|
|
707
719
|
gender: enduser?.gender,
|
|
@@ -709,6 +721,56 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
709
721
|
form,
|
|
710
722
|
}
|
|
711
723
|
|
|
724
|
+
// Auto-advance to target field when startingFieldId is provided
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
if (!startingFieldId || responses.length === 0 || autoAdvanceCompletedRef.current) return
|
|
727
|
+
|
|
728
|
+
// Start timing on first run
|
|
729
|
+
if (autoAdvanceStartTimeRef.current === null) {
|
|
730
|
+
autoAdvanceStartTimeRef.current = Date.now()
|
|
731
|
+
setIsAutoAdvancing(true)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const finishAutoAdvance = () => {
|
|
735
|
+
const elapsed = Date.now() - (autoAdvanceStartTimeRef.current || 0)
|
|
736
|
+
const remainingTime = Math.max(0, 1000 - elapsed) // Ensure at least 1 second
|
|
737
|
+
|
|
738
|
+
setTimeout(() => {
|
|
739
|
+
setIsAutoAdvancing(false)
|
|
740
|
+
autoAdvanceCompletedRef.current = true
|
|
741
|
+
}, remainingTime)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Check if we're already at the target
|
|
745
|
+
if (activeField.value.id === startingFieldId) {
|
|
746
|
+
finishAutoAdvance()
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Find current response
|
|
751
|
+
const currentResponse = responses.find(r => r.fieldId === activeField.value.id)
|
|
752
|
+
|
|
753
|
+
// If no response or no answer, we've reached the first unanswered question
|
|
754
|
+
if (!currentResponse || !currentResponse.answer?.value) {
|
|
755
|
+
finishAutoAdvance()
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Auto-advance to next field using existing logic
|
|
760
|
+
if (!prevFieldStackRef.current.find(v => v.value.id === activeField?.value.id)) {
|
|
761
|
+
prevFieldStackRef.current.push(activeField)
|
|
762
|
+
setCurrentPageIndex(i => i + 1)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const nextNode = getNextField(activeField, currentResponse, responses, logicOptions)
|
|
766
|
+
if (nextNode) {
|
|
767
|
+
setActiveField(nextNode)
|
|
768
|
+
// The useEffect will run again with the new activeField
|
|
769
|
+
} else {
|
|
770
|
+
finishAutoAdvance()
|
|
771
|
+
}
|
|
772
|
+
}, [startingFieldId, responses.length, activeField, logicOptions])
|
|
773
|
+
|
|
712
774
|
const handleDatabaseSelect = useCallback((databaseRecords: Pick<DatabaseRecord, "values" | "databaseId">[]) => {
|
|
713
775
|
try {
|
|
714
776
|
// no need to update if there's no prepopulation
|
|
@@ -1139,6 +1201,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
1139
1201
|
|
|
1140
1202
|
const handleFileUpload = useCallback(async (blob: FileBlob, fieldId: string) => {
|
|
1141
1203
|
const responseIndex = responses.findIndex(f => f.fieldId === fieldId)
|
|
1204
|
+
const field = fields.find(f => f.id === fieldId)
|
|
1142
1205
|
const result: FormResponseAnswerFileValue = { name: blob.name, secureName: '' }
|
|
1143
1206
|
const { secureName } = await handleUpload(
|
|
1144
1207
|
{
|
|
@@ -1146,7 +1209,8 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
1146
1209
|
size: blob.size,
|
|
1147
1210
|
type: blob.type,
|
|
1148
1211
|
enduserId,
|
|
1149
|
-
|
|
1212
|
+
hiddenFromEnduser: field?.options?.hideFromPortal,
|
|
1213
|
+
},
|
|
1150
1214
|
blob
|
|
1151
1215
|
)
|
|
1152
1216
|
|
|
@@ -1160,7 +1224,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
1160
1224
|
} else {
|
|
1161
1225
|
responses[responseIndex].answer.value = { ...result, type: blob.type, secureName, name: result.name ?? '' }
|
|
1162
1226
|
}
|
|
1163
|
-
}, [responses, handleUpload])
|
|
1227
|
+
}, [responses, handleUpload, fields])
|
|
1164
1228
|
|
|
1165
1229
|
const submit = useCallback(async (options?: { onPreRedirect?: () => void, onFileUploadsDone?: () => void, onSuccess?: (r: FormResponse) => void, includedFieldIds?: string[], otherEnduserIds?: string[], onBulkErrors?: (errors: { enduserId: string, message: string }[]) => void }) => {
|
|
1166
1230
|
setSubmitErrorMessage('')
|
|
@@ -1230,13 +1294,16 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
1230
1294
|
accessCode : (
|
|
1231
1295
|
accessCode
|
|
1232
1296
|
|| (await (
|
|
1233
|
-
session as any as Session).api.form_responses.prepare_form_response({
|
|
1297
|
+
session as any as Session).api.form_responses.prepare_form_response({
|
|
1234
1298
|
formId, enduserId: eId, isInternalNote, title: formTitle,
|
|
1235
1299
|
parentResponseId, rootResponseId,
|
|
1236
|
-
|
|
1237
|
-
carePlanId,
|
|
1300
|
+
|
|
1301
|
+
carePlanId,
|
|
1238
1302
|
context,
|
|
1239
1303
|
calendarEventId,
|
|
1304
|
+
groupId,
|
|
1305
|
+
groupInstance,
|
|
1306
|
+
groupPosition,
|
|
1240
1307
|
})
|
|
1241
1308
|
).accessCode
|
|
1242
1309
|
),
|
|
@@ -1536,5 +1603,6 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
|
|
|
1536
1603
|
logicOptions,
|
|
1537
1604
|
uploadingFiles, setUploadingFiles,
|
|
1538
1605
|
handleFileUpload,
|
|
1606
|
+
isAutoAdvancing,
|
|
1539
1607
|
}
|
|
1540
1608
|
}
|
package/src/Forms/inputs.tsx
CHANGED
|
@@ -1605,7 +1605,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
|
|
|
1605
1605
|
)
|
|
1606
1606
|
}
|
|
1607
1607
|
|
|
1608
|
-
export const StripeInput = ({ field, value, onChange, setCustomerId }: FormInputProps<'Stripe'> & {
|
|
1608
|
+
export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }: FormInputProps<'Stripe'> & {
|
|
1609
1609
|
setCustomerId: React.Dispatch<React.SetStateAction<string | undefined>>,
|
|
1610
1610
|
}) => {
|
|
1611
1611
|
const session = useResolvedSession()
|
|
@@ -1625,7 +1625,7 @@ export const StripeInput = ({ field, value, onChange, setCustomerId }: FormInput
|
|
|
1625
1625
|
}
|
|
1626
1626
|
fetchRef.current = true
|
|
1627
1627
|
|
|
1628
|
-
session.api.form_responses.stripe_details({ fieldId: field.id })
|
|
1628
|
+
session.api.form_responses.stripe_details({ fieldId: field.id, enduserId })
|
|
1629
1629
|
.then(({ clientSecret, publishableKey, stripeAccount, businessName, customerId, isCheckout, answerText }) => {
|
|
1630
1630
|
setAnswertext(answerText || '')
|
|
1631
1631
|
setIsCheckout(!!isCheckout)
|
|
@@ -1640,7 +1640,7 @@ export const StripeInput = ({ field, value, onChange, setCustomerId }: FormInput
|
|
|
1640
1640
|
setError(e.message)
|
|
1641
1641
|
}
|
|
1642
1642
|
})
|
|
1643
|
-
}, [session, value, field.id])
|
|
1643
|
+
}, [session, value, field.id, enduserId])
|
|
1644
1644
|
|
|
1645
1645
|
const cost = (
|
|
1646
1646
|
(field.options?.productIds || []).map(id => findProduct(id, { batch: false })) // seems to be having issues with bulk read
|