@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.
Files changed (46) hide show
  1. package/lib/cjs/CMS/ContentViewer.d.ts.map +1 -1
  2. package/lib/cjs/CMS/ContentViewer.js +28 -11
  3. package/lib/cjs/CMS/ContentViewer.js.map +1 -1
  4. package/lib/cjs/Forms/form_responses.d.ts.map +1 -1
  5. package/lib/cjs/Forms/form_responses.js +2 -2
  6. package/lib/cjs/Forms/form_responses.js.map +1 -1
  7. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  8. package/lib/cjs/Forms/forms.js +81 -9
  9. package/lib/cjs/Forms/forms.js.map +1 -1
  10. package/lib/cjs/Forms/hooks.d.ts +6 -1
  11. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  12. package/lib/cjs/Forms/hooks.js +66 -12
  13. package/lib/cjs/Forms/hooks.js.map +1 -1
  14. package/lib/cjs/Forms/inputs.d.ts +1 -1
  15. package/lib/cjs/Forms/inputs.js +3 -3
  16. package/lib/cjs/Forms/inputs.js.map +1 -1
  17. package/lib/cjs/state.d.ts.map +1 -1
  18. package/lib/cjs/state.js +50 -25
  19. package/lib/cjs/state.js.map +1 -1
  20. package/lib/esm/CMS/ContentViewer.d.ts.map +1 -1
  21. package/lib/esm/CMS/ContentViewer.js +28 -11
  22. package/lib/esm/CMS/ContentViewer.js.map +1 -1
  23. package/lib/esm/Forms/form_responses.d.ts.map +1 -1
  24. package/lib/esm/Forms/form_responses.js +2 -2
  25. package/lib/esm/Forms/form_responses.js.map +1 -1
  26. package/lib/esm/Forms/forms.d.ts.map +1 -1
  27. package/lib/esm/Forms/forms.js +82 -10
  28. package/lib/esm/Forms/forms.js.map +1 -1
  29. package/lib/esm/Forms/hooks.d.ts +6 -1
  30. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  31. package/lib/esm/Forms/hooks.js +66 -12
  32. package/lib/esm/Forms/hooks.js.map +1 -1
  33. package/lib/esm/Forms/inputs.d.ts +1 -1
  34. package/lib/esm/Forms/inputs.js +3 -3
  35. package/lib/esm/Forms/inputs.js.map +1 -1
  36. package/lib/esm/state.d.ts.map +1 -1
  37. package/lib/esm/state.js +50 -25
  38. package/lib/esm/state.js.map +1 -1
  39. package/lib/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +9 -9
  41. package/src/CMS/ContentViewer.tsx +38 -10
  42. package/src/Forms/form_responses.tsx +16 -14
  43. package/src/Forms/forms.tsx +129 -15
  44. package/src/Forms/hooks.tsx +77 -9
  45. package/src/Forms/inputs.tsx +3 -3
  46. 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.224.0",
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.224.0",
51
- "@tellescope/sdk": "^1.224.0",
52
- "@tellescope/types-client": "^1.224.0",
53
- "@tellescope/types-models": "^1.224.0",
54
- "@tellescope/types-utilities": "^1.224.0",
55
- "@tellescope/utilities": "^1.224.0",
56
- "@tellescope/validation": "^1.224.0",
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": "ed57eae72bced200381c5a7cf8703153886d2b4d",
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={{ fontSize: 18, lineHeight: '25pt' }}
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: 12 }}>
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
- <ResponseAnswer answer={r.answer} formResponse={response} fieldId={r.fieldId} />
355
+ <div style={{ }}>
356
+ <ResponseAnswer answer={r.answer} formResponse={response} fieldId={r.fieldId} />
357
+ </div>
356
358
  }
357
359
  </div>
358
360
  )
@@ -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, i) => {
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
 
@@ -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
- const [activeField, setActiveField] = useState(root)
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
  }
@@ -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