@tellescope/react-components 1.223.0 → 1.225.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 (55) 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 +96 -56
  9. package/lib/cjs/Forms/forms.js.map +1 -1
  10. package/lib/cjs/Forms/hooks.d.ts +4 -1
  11. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  12. package/lib/cjs/Forms/hooks.js +14 -9
  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/CMS/components.d.ts +0 -1
  24. package/lib/esm/CMS/components.d.ts.map +1 -1
  25. package/lib/esm/Forms/form_responses.d.ts.map +1 -1
  26. package/lib/esm/Forms/form_responses.js +2 -2
  27. package/lib/esm/Forms/form_responses.js.map +1 -1
  28. package/lib/esm/Forms/forms.d.ts +3 -3
  29. package/lib/esm/Forms/forms.d.ts.map +1 -1
  30. package/lib/esm/Forms/forms.js +97 -57
  31. package/lib/esm/Forms/forms.js.map +1 -1
  32. package/lib/esm/Forms/hooks.d.ts +4 -1
  33. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  34. package/lib/esm/Forms/hooks.js +14 -9
  35. package/lib/esm/Forms/hooks.js.map +1 -1
  36. package/lib/esm/Forms/inputs.d.ts +2 -2
  37. package/lib/esm/Forms/inputs.js +3 -3
  38. package/lib/esm/Forms/inputs.js.map +1 -1
  39. package/lib/esm/controls.d.ts +2 -2
  40. package/lib/esm/inputs.d.ts +1 -1
  41. package/lib/esm/inputs.native.d.ts +0 -1
  42. package/lib/esm/inputs.native.d.ts.map +1 -1
  43. package/lib/esm/layout.d.ts +1 -1
  44. package/lib/esm/state.d.ts +44 -44
  45. package/lib/esm/state.d.ts.map +1 -1
  46. package/lib/esm/state.js +50 -25
  47. package/lib/esm/state.js.map +1 -1
  48. package/lib/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +9 -9
  50. package/src/CMS/ContentViewer.tsx +38 -10
  51. package/src/Forms/form_responses.tsx +16 -14
  52. package/src/Forms/forms.tsx +73 -8
  53. package/src/Forms/hooks.tsx +14 -6
  54. package/src/Forms/inputs.tsx +3 -3
  55. 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.223.0",
3
+ "version": "1.225.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.223.0",
51
- "@tellescope/sdk": "^1.223.0",
52
- "@tellescope/types-client": "^1.223.0",
53
- "@tellescope/types-models": "^1.223.0",
54
- "@tellescope/types-utilities": "^1.223.0",
55
- "@tellescope/utilities": "^1.223.0",
56
- "@tellescope/validation": "^1.223.0",
50
+ "@tellescope/constants": "^1.225.0",
51
+ "@tellescope/sdk": "^1.225.0",
52
+ "@tellescope/types-client": "^1.225.0",
53
+ "@tellescope/types-models": "^1.225.0",
54
+ "@tellescope/types-utilities": "^1.225.0",
55
+ "@tellescope/utilities": "^1.225.0",
56
+ "@tellescope/validation": "^1.225.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": "c4041968a142a3ab45430b3f2dccfa0bbc36e3d9",
86
+ "gitHead": "764982e316e8cf0dba3151e4d0ecf3741bc39725",
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, 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 } : {
@@ -241,7 +241,27 @@ export const QuestionForField = ({
241
241
  }
242
242
 
243
243
  {
244
- field.type === 'file' ? (
244
+ // If field has pre-populated value and is set to be disabled when pre-populated, show as underlined text
245
+ field.disabledWhenPrepopulated && value.answer.value !== undefined && value.answer.value !== null && value.answer.value !== '' ? (
246
+ <div
247
+ style={{
248
+ padding: '8px 0',
249
+ borderBottom: '1px solid rgba(0, 0, 0, 0.42)',
250
+ width: '100%'
251
+ }}
252
+ >
253
+ <Typography
254
+ style={{
255
+ fontSize: '1rem',
256
+ color: 'rgba(0, 0, 0, 0.87)',
257
+ whiteSpace: 'pre-wrap'
258
+ }}
259
+ >
260
+ {form_response_value_to_string(value.answer.value)}
261
+ </Typography>
262
+ </div>
263
+ )
264
+ : field.type === 'file' ? (
245
265
  <File field={field} value={file.blobs?.[0] as any} onChange={onAddFile as any} form={form}
246
266
  existingFileName={
247
267
  value.answer.type === 'file'
@@ -295,7 +315,7 @@ export const QuestionForField = ({
295
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} />
296
316
  )
297
317
  : field.type === 'Stripe' ? (
298
- <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} />
299
319
  )
300
320
  : field.type === 'Chargebee' ? (
301
321
  <Chargebee field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Chargebee'>} setCustomerId={setCustomerId} form={form} />
@@ -554,6 +574,16 @@ export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
554
574
 
555
575
  const numRemainingPages = getNumberOfRemainingPages()
556
576
 
577
+ // Calculate current score if real-time scoring is enabled
578
+ const currentScores = useMemo(() => {
579
+ if (!form?.realTimeScoring || !form.scoring?.length) return null
580
+
581
+ return calculate_form_scoring({
582
+ response: { responses },
583
+ form: { scoring: form.scoring }
584
+ })
585
+ }, [form?.realTimeScoring, form?.scoring, responses])
586
+
557
587
  if (!(currentValue && currentFileValue)) return <></>
558
588
  return (
559
589
  submitted
@@ -570,7 +600,7 @@ export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
570
600
  handleDatabaseSelect={handleDatabaseSelect}
571
601
  setCustomerId={setCustomerId}
572
602
  repeats={repeats} onRepeatsChange={setRepeats}
573
- value={currentValue} file={currentFileValue}
603
+ value={currentValue} file={currentFileValue}
574
604
  customInputs={customInputs}
575
605
  onAddFile={onAddFile} onFieldChange={onFieldChange}
576
606
  responses={responses} selectedFiles={selectedFiles}
@@ -638,14 +668,49 @@ export const TellescopeSingleQuestionFlow: typeof TellescopeForm = ({
638
668
  }
639
669
  </Flex>
640
670
 
641
- {!customization?.hideProgressBar &&
642
- <Progress
643
- numerator={currentPageIndex + (validateCurrentField() ? 0 : 1)}
644
- denominator={currentPageIndex + 1 + numRemainingPages}
671
+ {!customization?.hideProgressBar &&
672
+ <Progress
673
+ numerator={currentPageIndex + (validateCurrentField() ? 0 : 1)}
674
+ denominator={currentPageIndex + 1 + numRemainingPages}
645
675
  style={{ marginTop: '15px' }}
646
676
  />
647
677
  }
648
678
 
679
+ {/* Real-time scoring display */}
680
+ {currentScores && currentScores.length > 0 && (
681
+ <Flex style={{ marginTop: 10, marginBottom: 5, width: '100%' }}>
682
+ {currentScores.map((score, index) => (
683
+ <Flex key={index} style={{
684
+ padding: '10px 14px',
685
+ backgroundColor: '#f8f9fa',
686
+ borderRadius: 8,
687
+ border: `1px solid ${theme?.themeColor || PRIMARY_HEX}20`,
688
+ marginRight: index < currentScores.length - 1 ? 12 : 0,
689
+ minWidth: 120,
690
+ flexDirection: 'column',
691
+ alignItems: 'center'
692
+ }}>
693
+ <Typography style={{
694
+ fontSize: 12,
695
+ fontWeight: 'medium',
696
+ textAlign: 'center',
697
+ lineHeight: 1.2,
698
+ marginBottom: 4
699
+ }}>
700
+ {score.title}
701
+ </Typography>
702
+ <Typography style={{
703
+ fontWeight: 'bold',
704
+ color: theme?.themeColor || PRIMARY_HEX,
705
+ fontSize: 18
706
+ }}>
707
+ {score.value}
708
+ </Typography>
709
+ </Flex>
710
+ ))}
711
+ </Flex>
712
+ )}
713
+
649
714
  <Typography color="error" style={{ alignText: 'center', marginTop: 3 }}>
650
715
  {submitErrorMessage}
651
716
  </Typography>
@@ -364,6 +364,9 @@ interface UseTellescopeFormOptions {
364
364
  enduser?: Partial<Enduser>,
365
365
  isPublicForm?: boolean,
366
366
  dontAutoadvance?: boolean,
367
+ groupId?: string,
368
+ groupInstance?: string,
369
+ groupPosition?: number,
367
370
  }
368
371
 
369
372
  const OrganizationThemeContext = createContext(null as any as {
@@ -512,7 +515,7 @@ const shouldCallout = (field: FormField | undefined, value: FormResponseValueAns
512
515
 
513
516
  export type Response = FormResponseValue & { touched: boolean, includeInSubmit: boolean, field: FormField }
514
517
  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) => {
518
+ 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 }: UseTellescopeFormOptions) => {
516
519
  const { amPm, hoursAmPm, minutes } = get_time_values(new Date())
517
520
 
518
521
  const root = useTreeForFormFields(fields)
@@ -1139,6 +1142,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1139
1142
 
1140
1143
  const handleFileUpload = useCallback(async (blob: FileBlob, fieldId: string) => {
1141
1144
  const responseIndex = responses.findIndex(f => f.fieldId === fieldId)
1145
+ const field = fields.find(f => f.id === fieldId)
1142
1146
  const result: FormResponseAnswerFileValue = { name: blob.name, secureName: '' }
1143
1147
  const { secureName } = await handleUpload(
1144
1148
  {
@@ -1146,7 +1150,8 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1146
1150
  size: blob.size,
1147
1151
  type: blob.type,
1148
1152
  enduserId,
1149
- },
1153
+ hiddenFromEnduser: field?.options?.hideFromPortal,
1154
+ },
1150
1155
  blob
1151
1156
  )
1152
1157
 
@@ -1160,7 +1165,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1160
1165
  } else {
1161
1166
  responses[responseIndex].answer.value = { ...result, type: blob.type, secureName, name: result.name ?? '' }
1162
1167
  }
1163
- }, [responses, handleUpload])
1168
+ }, [responses, handleUpload, fields])
1164
1169
 
1165
1170
  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
1171
  setSubmitErrorMessage('')
@@ -1230,13 +1235,16 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1230
1235
  accessCode : (
1231
1236
  accessCode
1232
1237
  || (await (
1233
- session as any as Session).api.form_responses.prepare_form_response({
1238
+ session as any as Session).api.form_responses.prepare_form_response({
1234
1239
  formId, enduserId: eId, isInternalNote, title: formTitle,
1235
1240
  parentResponseId, rootResponseId,
1236
-
1237
- carePlanId,
1241
+
1242
+ carePlanId,
1238
1243
  context,
1239
1244
  calendarEventId,
1245
+ groupId,
1246
+ groupInstance,
1247
+ groupPosition,
1240
1248
  })
1241
1249
  ).accessCode
1242
1250
  ),
@@ -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
package/src/state.tsx CHANGED
@@ -887,32 +887,29 @@ export const useListStateHook = <T extends { id: string | number }, ADD extends
887
887
  // prevent frequent refetches
888
888
  if (value && options?.reload && didFetch('findById' + modelName + id, true)) return value
889
889
 
890
- if (options?.reload || (value === undefined && !didFetch('findById' + modelName + id))) {
891
- setFetched('findById' + modelName + id, true) // prevent multiple API calls
892
-
893
- if (options?.batch) {
894
- // ensure duplicate ids are not provided
895
- if (!batchRef.current.nextBatch.includes(id.toString()) && !batchRef.current.ids.includes(id.toString())) {
896
- if (batchRef.current.fetching) {
897
- batchRef.current.nextBatch.push(id.toString()) // fetch in next batch if currently fetching
898
- } else {
899
- batchRef.current.ids.push(id.toString())
900
- }
890
+ // Handle batch requests separately - always allow queueing for batch
891
+ if (options?.batch) {
892
+ if (!batchRef.current.nextBatch.includes(id.toString()) && !batchRef.current.ids.includes(id.toString())) {
893
+ if (batchRef.current.fetching) {
894
+ batchRef.current.nextBatch.push(id.toString()) // fetch in next batch if currently fetching
895
+ } else {
896
+ batchRef.current.ids.push(id.toString())
901
897
  }
902
- } else {
903
- findOne?.(id.toString())
904
- .then(found => {
905
- // prevent unnecessary re-renders by calling addLocalElement, when the exact value already exists
906
- const existingUnchanged = value_is_loaded(state) && state.value.find(v => v.id === id && objects_equivalent(v, found))
907
- if (existingUnchanged) return
908
-
909
- addLocalElement(found, { replaceIfMatch: true })
910
- })
911
- .catch(e => {
912
- setFetched('recordNotFound' + modelName + id, true) // mark record not found for id
913
- console.error(e)
914
- })
915
898
  }
899
+ } else if (options?.reload || (value === undefined && !didFetch('findById' + modelName + id))) {
900
+ setFetched('findById' + modelName + id, true) // prevent multiple API calls
901
+ findOne?.(id.toString())
902
+ .then(found => {
903
+ // prevent unnecessary re-renders by calling addLocalElement, when the exact value already exists
904
+ const existingUnchanged = value_is_loaded(state) && state.value.find(v => v.id === id && objects_equivalent(v, found))
905
+ if (existingUnchanged) return
906
+
907
+ addLocalElement(found, { replaceIfMatch: true })
908
+ })
909
+ .catch(e => {
910
+ setFetched('recordNotFound' + modelName + id, true) // mark record not found for id
911
+ console.error(e)
912
+ })
916
913
  }
917
914
 
918
915
  return value
@@ -931,30 +928,61 @@ export const useListStateHook = <T extends { id: string | number }, ADD extends
931
928
  }
932
929
 
933
930
  batchRef.current.fetching = true
931
+ const currentBatchIds = batchRef.current.ids.slice(0, BULK_READ_DEFAULT_LIMIT)
932
+
933
+ // Filter out IDs that are already loaded to prevent unnecessary fetching
934
+ const idsToFetch = currentBatchIds.filter(id => {
935
+ const alreadyLoaded = state.status === LoadingStatus.Loaded &&
936
+ state.value.find(v => v.id.toString() === id.toString())
937
+ return !alreadyLoaded
938
+ })
939
+
940
+ // Only make API call if there are actually IDs to fetch
941
+ if (idsToFetch.length === 0) {
942
+ // Mark all IDs as processed even though they were already loaded
943
+ currentBatchIds.forEach(id => {
944
+ setFetched('findById' + modelName + id, true)
945
+ })
946
+
947
+ // Continue to next batch
948
+ batchRef.current.ids = batchRef.current.nextBatch
949
+ batchRef.current.nextBatch = []
950
+ batchRef.current.fetching = false
951
+ return
952
+ }
953
+
934
954
  findByIds({
935
- ids: batchRef.current.ids.slice(0, BULK_READ_DEFAULT_LIMIT), // ensure limited to 1000 entries
955
+ ids: idsToFetch,
936
956
  })
937
957
  .then(({ matches }) => {
938
- if (matches.length) {
958
+ if (matches.length) {
939
959
  addLocalElements(matches, { replaceIfMatch: true })
940
- options?.onBulkRead?.(matches)
960
+ options?.onBulkRead?.(matches)
941
961
  }
962
+ // Mark all fetched IDs as complete (both successful and failed)
963
+ currentBatchIds.forEach(id => {
964
+ setFetched('findById' + modelName + id, true)
965
+ })
942
966
  })
943
967
  .catch(err => {
944
968
  console.error(err)
969
+ // Mark failed IDs as not found
970
+ currentBatchIds.forEach(id => {
971
+ setFetched('recordNotFound' + modelName + id, true)
972
+ })
945
973
  })
946
974
  .finally(() => {
947
975
  // ensure we make progress to prevent looping on an error
948
976
  batchRef.current.ids = batchRef.current.nextBatch
949
977
  batchRef.current.nextBatch = []
950
-
978
+
951
979
  // allow next fetch
952
980
  batchRef.current.fetching = false
953
981
  })
954
982
  }, 333)
955
983
 
956
984
  return () => { clearInterval(i) }
957
- }, [findByIds, addLocalElements, options?.onBulkRead])
985
+ }, [findByIds, addLocalElements, options?.onBulkRead, setFetched, modelName])
958
986
 
959
987
  const findByFilter: ListUpdateMethods<T, ADD>['findByFilter'] = useCallback((filter, options) => {
960
988
  const loadFilter = options?.loadFilter