@tellescope/react-components 1.249.1 → 1.250.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 (66) hide show
  1. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  2. package/lib/cjs/Forms/forms.js +13 -5
  3. package/lib/cjs/Forms/forms.js.map +1 -1
  4. package/lib/cjs/Forms/forms.v2.d.ts.map +1 -1
  5. package/lib/cjs/Forms/forms.v2.js +14 -6
  6. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  7. package/lib/cjs/Forms/hooks.d.ts +2 -1
  8. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  9. package/lib/cjs/Forms/hooks.js +49 -26
  10. package/lib/cjs/Forms/hooks.js.map +1 -1
  11. package/lib/cjs/Forms/inputs.d.ts +19 -4
  12. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  13. package/lib/cjs/Forms/inputs.js +224 -173
  14. package/lib/cjs/Forms/inputs.js.map +1 -1
  15. package/lib/cjs/Forms/inputs.v2.d.ts +7 -3
  16. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  17. package/lib/cjs/Forms/inputs.v2.js +42 -32
  18. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  19. package/lib/cjs/TwilioVideo/TwilioControls.d.ts.map +1 -1
  20. package/lib/cjs/TwilioVideo/TwilioControls.js +12 -2
  21. package/lib/cjs/TwilioVideo/TwilioControls.js.map +1 -1
  22. package/lib/cjs/TwilioVideo/TwilioLocalPreview.d.ts.map +1 -1
  23. package/lib/cjs/TwilioVideo/TwilioLocalPreview.js +154 -2
  24. package/lib/cjs/TwilioVideo/TwilioLocalPreview.js.map +1 -1
  25. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts +7 -0
  26. package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
  27. package/lib/cjs/TwilioVideo/TwilioVideoContext.js +148 -1
  28. package/lib/cjs/TwilioVideo/TwilioVideoContext.js.map +1 -1
  29. package/lib/esm/Forms/forms.d.ts.map +1 -1
  30. package/lib/esm/Forms/forms.js +13 -5
  31. package/lib/esm/Forms/forms.js.map +1 -1
  32. package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
  33. package/lib/esm/Forms/forms.v2.js +14 -6
  34. package/lib/esm/Forms/forms.v2.js.map +1 -1
  35. package/lib/esm/Forms/hooks.d.ts +2 -1
  36. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  37. package/lib/esm/Forms/hooks.js +49 -26
  38. package/lib/esm/Forms/hooks.js.map +1 -1
  39. package/lib/esm/Forms/inputs.d.ts +19 -4
  40. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  41. package/lib/esm/Forms/inputs.js +69 -20
  42. package/lib/esm/Forms/inputs.js.map +1 -1
  43. package/lib/esm/Forms/inputs.v2.d.ts +7 -3
  44. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  45. package/lib/esm/Forms/inputs.v2.js +27 -17
  46. package/lib/esm/Forms/inputs.v2.js.map +1 -1
  47. package/lib/esm/TwilioVideo/TwilioControls.d.ts.map +1 -1
  48. package/lib/esm/TwilioVideo/TwilioControls.js +14 -4
  49. package/lib/esm/TwilioVideo/TwilioControls.js.map +1 -1
  50. package/lib/esm/TwilioVideo/TwilioLocalPreview.d.ts.map +1 -1
  51. package/lib/esm/TwilioVideo/TwilioLocalPreview.js +155 -3
  52. package/lib/esm/TwilioVideo/TwilioLocalPreview.js.map +1 -1
  53. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts +7 -0
  54. package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
  55. package/lib/esm/TwilioVideo/TwilioVideoContext.js +146 -0
  56. package/lib/esm/TwilioVideo/TwilioVideoContext.js.map +1 -1
  57. package/lib/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +11 -10
  59. package/src/Forms/forms.tsx +18 -2
  60. package/src/Forms/forms.v2.tsx +19 -3
  61. package/src/Forms/hooks.tsx +67 -30
  62. package/src/Forms/inputs.tsx +143 -18
  63. package/src/Forms/inputs.v2.tsx +58 -8
  64. package/src/TwilioVideo/TwilioControls.tsx +27 -1
  65. package/src/TwilioVideo/TwilioLocalPreview.tsx +136 -1
  66. package/src/TwilioVideo/TwilioVideoContext.tsx +126 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.249.1",
3
+ "version": "1.250.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -51,13 +51,14 @@
51
51
  "@reduxjs/toolkit": "1.9.0",
52
52
  "@stripe/react-stripe-js": "2.9.0",
53
53
  "@stripe/stripe-js": "1.52.1",
54
- "@tellescope/constants": "1.249.1",
55
- "@tellescope/sdk": "1.249.1",
56
- "@tellescope/types-client": "1.249.1",
57
- "@tellescope/types-models": "1.249.1",
58
- "@tellescope/types-utilities": "1.249.1",
59
- "@tellescope/utilities": "1.249.1",
60
- "@tellescope/validation": "1.249.1",
54
+ "@tellescope/constants": "1.250.0",
55
+ "@tellescope/sdk": "1.250.0",
56
+ "@tellescope/types-client": "1.250.0",
57
+ "@tellescope/types-models": "1.250.0",
58
+ "@tellescope/types-utilities": "1.250.0",
59
+ "@tellescope/utilities": "1.250.0",
60
+ "@tellescope/validation": "1.250.0",
61
+ "@twilio/video-processors": "3.2.0",
61
62
  "css-to-react-native": "3.0.0",
62
63
  "draft-js": "0.11.7",
63
64
  "draftjs-to-html": "0.9.1",
@@ -76,7 +77,7 @@
76
77
  "react-native-video": "5.2.1",
77
78
  "react-redux": "7.2.9",
78
79
  "react-window": "1.8.9",
79
- "twilio-video": "^2.28.1",
80
+ "twilio-video": "2.35.0",
80
81
  "yup": "0.32.11"
81
82
  },
82
83
  "peerDependencies": {
@@ -84,7 +85,7 @@
84
85
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
85
86
  "react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
86
87
  },
87
- "gitHead": "23480776738c31e1d56244ce6c23d9abe032e15a",
88
+ "gitHead": "dc1bcee546276fd38231a5ac672efd21761fe8e8",
88
89
  "publishConfig": {
89
90
  "access": "public"
90
91
  }
@@ -272,22 +272,38 @@ export const QuestionForField = ({
272
272
  )
273
273
  : field.type === 'file' ? (
274
274
  <File field={field} value={file.blobs?.[0] as any} onChange={onAddFile as any} form={form}
275
+ enduserId={enduserId}
275
276
  existingFileName={
276
277
  value.answer.type === 'file'
277
278
  ? value.answer.value?.name
278
279
  : ''
279
- }
280
+ }
280
281
  handleFileUpload={handleFileUpload} uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
282
+ onSelectExistingFile={v => onFieldChange(v as any, field.id)}
281
283
  />
282
284
  )
283
285
  : field.type === 'files' ? (
284
286
  <Files field={field} value={file.blobs as any} onChange={onAddFile as any} form={form}
287
+ enduserId={enduserId}
285
288
  // existingFileName={
286
289
  // value.answer.type === 'files'
287
290
  // ? value.answer.value?.name
288
291
  // : ''
289
- // }
292
+ // }
290
293
  handleFileUpload={handleFileUpload} uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
294
+ existingSelections={
295
+ value.answer.type === 'files' && Array.isArray(value.answer.value)
296
+ ? value.answer.value.filter(av => !file.blobs?.some(b => b.name === av.name))
297
+ : undefined
298
+ }
299
+ onSelectExistingFile={v => {
300
+ const current = value.answer.type === 'files' && Array.isArray(value.answer.value) ? value.answer.value : []
301
+ onFieldChange([...current, v] as any, field.id)
302
+ }}
303
+ onRemoveExistingFile={secureName => {
304
+ const current = value.answer.type === 'files' && Array.isArray(value.answer.value) ? value.answer.value : []
305
+ onFieldChange(current.filter(f => f.secureName !== secureName) as any, field.id)
306
+ }}
291
307
  />
292
308
  )
293
309
  : field.type === 'dateString' ? (
@@ -49,7 +49,7 @@ const TellescopeFormContainerWithThemeV2: typeof TellescopeFormContainerV2 = ({
49
49
 
50
50
  return (
51
51
  <Flex flex={1} column alignItems="center" style={{
52
- backgroundColor: finalBgColor,
52
+ backgroundColor: hideBg ? 'transparent' : finalBgColor,
53
53
  overflow: 'auto',
54
54
  boxSizing: 'border-box',
55
55
  paddingTop: window.innerWidth < 600 ? 20 : 40,
@@ -282,22 +282,38 @@ export const QuestionForField = ({
282
282
  )
283
283
  : field.type === 'file' ? (
284
284
  <File field={field} value={file.blobs?.[0] as any} onChange={onAddFile as any} form={form}
285
+ enduserId={enduserId}
285
286
  existingFileName={
286
287
  value.answer.type === 'file'
287
288
  ? value.answer.value?.name
288
289
  : ''
289
- }
290
+ }
290
291
  handleFileUpload={handleFileUpload} uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
292
+ onSelectExistingFile={v => onFieldChange(v as any, field.id)}
291
293
  />
292
294
  )
293
295
  : field.type === 'files' ? (
294
296
  <Files field={field} value={file.blobs as any} onChange={onAddFile as any} form={form}
297
+ enduserId={enduserId}
295
298
  // existingFileName={
296
299
  // value.answer.type === 'files'
297
300
  // ? value.answer.value?.name
298
301
  // : ''
299
- // }
302
+ // }
300
303
  handleFileUpload={handleFileUpload} uploadingFiles={uploadingFiles} setUploadingFiles={setUploadingFiles}
304
+ existingSelections={
305
+ value.answer.type === 'files' && Array.isArray(value.answer.value)
306
+ ? value.answer.value.filter(av => !file.blobs?.some(b => b.name === av.name))
307
+ : undefined
308
+ }
309
+ onSelectExistingFile={v => {
310
+ const current = value.answer.type === 'files' && Array.isArray(value.answer.value) ? value.answer.value : []
311
+ onFieldChange([...current, v] as any, field.id)
312
+ }}
313
+ onRemoveExistingFile={secureName => {
314
+ const current = value.answer.type === 'files' && Array.isArray(value.answer.value) ? value.answer.value : []
315
+ onFieldChange(current.filter(f => f.secureName !== secureName) as any, field.id)
316
+ }}
301
317
  />
302
318
  )
303
319
  : field.type === 'dateString' ? (
@@ -374,9 +374,10 @@ interface UseTellescopeFormOptions {
374
374
  groupId?: string,
375
375
  groupInstance?: string,
376
376
  groupPosition?: number,
377
+ getEnduserAISummary?: () => string | undefined,
377
378
  }
378
379
 
379
- const OrganizationThemeContext = createContext(null as any as {
380
+ const OrganizationThemeContext = createContext(null as any as {
380
381
  theme: OrganizationTheme,
381
382
  setTheme: (theme: OrganizationTheme) => void,
382
383
  businessId?: string,
@@ -558,7 +559,7 @@ const shouldCallout = (field: FormField | undefined, value: FormResponseValueAns
558
559
 
559
560
  export type Response = FormResponseValue & { touched: boolean, includeInSubmit: boolean, field: FormField }
560
561
  export type FileResponse = { fieldId: string, fieldTitle: string, blobs?: FileBlob[] }
561
- 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) => {
562
+ 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, getEnduserAISummary }: UseTellescopeFormOptions) => {
562
563
  const { amPm, hoursAmPm, minutes } = get_time_values(new Date())
563
564
 
564
565
  const root = useTreeForFormFields(fields)
@@ -1504,6 +1505,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1504
1505
  customerId,
1505
1506
  productIds: responsesToSubmit.flatMap(r => r.field?.options?.productIds ?? []),
1506
1507
  utm: get_utm_params(),
1508
+ ...(getEnduserAISummary ? { enduserAISummary: getEnduserAISummary() } : {}),
1507
1509
  })
1508
1510
 
1509
1511
  // do actual redirect later to prevent popup
@@ -1678,33 +1680,68 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1678
1680
  autoAdvanceRef.current = true
1679
1681
  }
1680
1682
 
1681
- setResponses(rs => rs.map(r => r.fieldId !== fieldId ? r : ({
1682
- ...r,
1683
- touched,
1684
- isCalledOut: shouldCallout(fields?.find(f => f?.id === fieldId), value),
1685
- isHighlightedOnTimeline: fields?.find(f => f?.id === fieldId)?.highlightOnTimeline,
1686
- // description fields are never "active", so the normal updateInclusion effect won't fire for them
1687
- // explicitly mark as included when they receive a non-empty string value (historical data snapshot)
1688
- ...(field?.type === 'description' && typeof value === 'string' && value ? { includeInSubmit: true } : {}),
1689
- answer: {
1690
- ...r.answer,
1691
- value: value as any,
1692
- },
1693
- // keep consistent with initialize existing responses
1694
- computedValueKey: (
1695
- field?.intakeField === 'height'
1696
- ? 'Height'
1697
- : field?.intakeField === 'weight' && typeof value === 'number'
1698
- ? 'Weight'
1699
- : field?.intakeField === 'dateOfBirth' && r.answer.type === 'dateString'
1700
- ? 'Date of Birth'
1701
- : field?.intakeField === 'gender' && (r.answer.type === 'Dropdown' || r.answer.type === 'multiple_choice')
1702
- ? 'Gender'
1703
- : field?.intakeField === 'Address' && r.answer.type === 'Address'
1704
- ? 'State'
1705
- : undefined
1706
- )
1707
- })))
1683
+ setResponses(rs => {
1684
+ const updated = rs.map(r => r.fieldId !== fieldId ? r : ({
1685
+ ...r,
1686
+ touched,
1687
+ isCalledOut: shouldCallout(fields?.find(f => f?.id === fieldId), value),
1688
+ isHighlightedOnTimeline: fields?.find(f => f?.id === fieldId)?.highlightOnTimeline,
1689
+ // description fields are never "active", so the normal updateInclusion effect won't fire for them
1690
+ // explicitly mark as included when they receive a non-empty string value (historical data snapshot)
1691
+ ...(field?.type === 'description' && typeof value === 'string' && value ? { includeInSubmit: true } : {}),
1692
+ answer: {
1693
+ ...r.answer,
1694
+ value: value as any,
1695
+ },
1696
+ // keep consistent with initialize existing responses
1697
+ computedValueKey: (
1698
+ field?.intakeField === 'height'
1699
+ ? 'Height' as const
1700
+ : field?.intakeField === 'weight' && typeof value === 'number'
1701
+ ? 'Weight' as const
1702
+ : field?.intakeField === 'dateOfBirth' && r.answer.type === 'dateString'
1703
+ ? 'Date of Birth' as const
1704
+ : field?.intakeField === 'gender' && (r.answer.type === 'Dropdown' || r.answer.type === 'multiple_choice')
1705
+ ? 'Gender' as const
1706
+ : field?.intakeField === 'Address' && r.answer.type === 'Address'
1707
+ ? 'State' as const
1708
+ : undefined
1709
+ )
1710
+ }))
1711
+
1712
+ // Re-apply filter_stale_choices to every other multiple_choice/Dropdown response now that an upstream
1713
+ // answer may have changed which option-level showCondition results have flipped. Without this cascade,
1714
+ // selections made earlier in the session for options that are no longer visible would silently persist
1715
+ // and pass validation since the stored array remains non-empty.
1716
+ return updated.map(r => {
1717
+ if (r.fieldId === fieldId) return r
1718
+ if (r.answer.type !== 'multiple_choice' && r.answer.type !== 'Dropdown') return r
1719
+ const otherField = fields.find(f => f.id === r.fieldId)
1720
+ if (!otherField) return r
1721
+ if (otherField.type !== 'multiple_choice' && otherField.type !== 'Dropdown') return r
1722
+
1723
+ const prior = r.answer.value as string[] | undefined
1724
+ if (!Array.isArray(prior)) return r
1725
+
1726
+ const filtered = filter_stale_choices(prior, otherField, updated, enduser, form)
1727
+ if (!Array.isArray(filtered)) return r
1728
+
1729
+ // shallow array comparison — preserve reference if unchanged so memoized derivations don't churn
1730
+ if (filtered.length === prior.length && filtered.every((v, i) => v === prior[i])) {
1731
+ return r
1732
+ }
1733
+
1734
+ const isEmpty = filtered.length === 0
1735
+ return {
1736
+ ...r,
1737
+ ...(isEmpty ? { touched: false, includeInSubmit: false } : {}),
1738
+ answer: {
1739
+ ...r.answer,
1740
+ value: filtered as any,
1741
+ },
1742
+ }
1743
+ })
1744
+ })
1708
1745
 
1709
1746
  // ensure stripe payment is stored as saved immediately
1710
1747
  const saveField = fields.find(f => f.id === fieldId && (f.type === 'Stripe' || f.type === 'Appointment Booking'))
@@ -1746,7 +1783,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1746
1783
  })
1747
1784
  .catch(console.error)
1748
1785
  }
1749
- }, [fields])
1786
+ }, [fields, enduser, form])
1750
1787
 
1751
1788
  const onAddFile = useCallback((blobs?: FileBlob | FileBlob[], fieldId=activeField.value.id) => {
1752
1789
  setSelectedFiles(fs => fs.map(f => f.fieldId !== fieldId ? f : ({
@@ -5,15 +5,15 @@ import { FormInputProps } from "./types"
5
5
  import { useDropzone } from "react-dropzone"
6
6
  import { CANVAS_TITLE, BRIDGE_TITLE, CANDID_TITLE, EMOTII_TITLE, INSURANCE_RELATIONSHIPS, INSURANCE_RELATIONSHIPS_CANVAS, PRIMARY_HEX, RELATIONSHIP_TYPES, TELLESCOPE_GENDERS } from "@tellescope/constants"
7
7
  import { MM_DD_YYYY_to_YYYY_MM_DD, capture_is_supported, downloadFile, emit_gtm_event, first_letter_capitalized, form_response_value_to_string, format_stripe_subscription_interval, getLocalTimezone, getPublicFileURL, mm_dd_yyyy, object_is_empty, replace_enduser_template_values, responses_satisfy_conditions, truncate_string, update_local_storage, user_display_name } from "@tellescope/utilities"
8
- import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, FormFieldOptionDetails, Pharmacy, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
8
+ import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseAnswerFileValue, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, FormFieldOptionDetails, Pharmacy, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
9
9
  import { VALID_STATES, emailValidator, phoneValidator } from "@tellescope/validation"
10
10
  import Slider from '@mui/material/Slider';
11
11
  import LinearProgress from '@mui/material/LinearProgress';
12
12
 
13
13
  import DatePicker from "react-datepicker";
14
14
  import { datepickerCSS } from "./css/react-datepicker" // avoids build issue with RN
15
- import { CancelIcon, FileBlob, IconButton, LabeledIconButton, LoadingButton, Styled, form_display_text_for_language, isDateString, useProducts, useResolvedSession } from ".."
16
- import { CalendarEvent, DatabaseRecord, Form, FormField } from "@tellescope/types-client"
15
+ import { CancelIcon, FileBlob, IconButton, LabeledIconButton, LoadingButton, Styled, form_display_text_for_language, isDateString, useFiles, useProducts, useResolvedSession, value_is_loaded } from ".."
16
+ import { CalendarEvent, DatabaseRecord, File as TellescopeFile, Form, FormField } from "@tellescope/types-client"
17
17
  import { css } from '@emotion/css'
18
18
  import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
19
19
  import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
@@ -650,12 +650,18 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
650
650
  <Grid item xs={12} sm={6}>
651
651
  <Autocomplete freeSolo={!field.options?.requirePredefinedInsurer} options={payers.map(p => p.name)}
652
652
  value={value?.payerName || ''}
653
- onChange={(e, v) => onChange({
654
- ...value,
655
- payerName: v || '',
656
- payerId: payers.find(p => p.name === v)?.id || '',
657
- payerType: payers.find(p => p.name === v)?.type || '',
658
- }, field.id)}
653
+ onChange={(e, v) => {
654
+ const matched = payers.find(p => p.name === v)
655
+ if (matched?.databaseRecord) {
656
+ onDatabaseSelect?.([matched.databaseRecord])
657
+ }
658
+ onChange({
659
+ ...value,
660
+ payerName: v || '',
661
+ payerId: matched?.id || '',
662
+ payerType: matched?.type || '',
663
+ }, field.id)
664
+ }}
659
665
  onInputChange={
660
666
  field.options?.requirePredefinedInsurer
661
667
  ? (e, v) => { if (v) { setQuery(v) } }
@@ -2246,7 +2252,79 @@ export async function convertHEIC (file: FileBlob | string){
2246
2252
  };
2247
2253
 
2248
2254
  const value_is_image = (f?: { type?: string })=> f?.type?.includes('image')
2249
- export const FileInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles, form }: FormInputProps<'file'> & { existingFileName?: string }) => {
2255
+
2256
+ const fileMatchesValidTypes = (file: { type?: string }, validFileTypes?: string[]) => {
2257
+ if (!validFileTypes?.length) return true
2258
+ if (!file.type) return false
2259
+ return !!validFileTypes.find(t => file.type!.includes(t.toLowerCase()))
2260
+ }
2261
+
2262
+ export const ExistingFilePicker = ({ enduserId, excludedSecureNames, validFileTypes, onSelect, form, label }: {
2263
+ enduserId?: string,
2264
+ excludedSecureNames?: string[],
2265
+ validFileTypes?: string[],
2266
+ onSelect: (file: TellescopeFile) => void,
2267
+ form?: Form,
2268
+ label?: string,
2269
+ }) => {
2270
+ const session = useResolvedSession()
2271
+ const isEnduserSession = session.type === 'enduser'
2272
+
2273
+ const [, { filtered: getFiltered }] = useFiles({
2274
+ loadFilter: { enduserId },
2275
+ dontFetch: !enduserId || isEnduserSession,
2276
+ })
2277
+ const filesLoading = getFiltered(e => (!!enduserId) && (e.enduserId === enduserId))
2278
+
2279
+ const filtered = useMemo(() => {
2280
+ if (!value_is_loaded(filesLoading)) return []
2281
+ return filesLoading.value.filter(f => (
2282
+ !!f.confirmedAt
2283
+ && fileMatchesValidTypes(f, validFileTypes)
2284
+ && !excludedSecureNames?.includes(f.secureName)
2285
+ ))
2286
+ }, [filesLoading, validFileTypes, excludedSecureNames])
2287
+
2288
+ // Only available in User (staff) sessions — endusers must upload.
2289
+ if (isEnduserSession) return null
2290
+ if (!enduserId) return null
2291
+ if (filtered.length === 0) return null
2292
+
2293
+ return (
2294
+ <Grid item sx={{ mt: 1 }}>
2295
+ <Autocomplete<TellescopeFile>
2296
+ size="small"
2297
+ options={filtered}
2298
+ getOptionLabel={f => f.name}
2299
+ renderOption={(props, option) => (
2300
+ <li {...props} key={option.id}>
2301
+ <Grid container direction="column">
2302
+ <Typography sx={{ fontSize: 14 }}>{option.name}</Typography>
2303
+ {option.timestamp && (
2304
+ <Typography sx={{ fontSize: 12, color: '#666' }}>
2305
+ {new Date(option.timestamp).toLocaleDateString()}
2306
+ </Typography>
2307
+ )}
2308
+ </Grid>
2309
+ </li>
2310
+ )}
2311
+ onChange={(_, value) => {
2312
+ if (value) onSelect(value)
2313
+ }}
2314
+ value={null}
2315
+ blurOnSelect
2316
+ clearOnBlur
2317
+ renderInput={params => (
2318
+ <TextField {...params}
2319
+ label={label || form_display_text_for_language(form, "Or select an existing file from this patient")}
2320
+ />
2321
+ )}
2322
+ />
2323
+ </Grid>
2324
+ )
2325
+ }
2326
+
2327
+ export const FileInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles, form, enduserId, onSelectExistingFile }: FormInputProps<'file'> & { existingFileName?: string, onSelectExistingFile?: (value: FormResponseAnswerFileValue) => void }) => {
2250
2328
  const [error, setError] = useState('')
2251
2329
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
2252
2330
  onDrop: useCallback(
@@ -2341,17 +2419,28 @@ export const FileInput = ({ value, onChange, field, existingFileName, uploadingF
2341
2419
  </Grid>
2342
2420
 
2343
2421
  <Grid item alignSelf="center" sx={{ mt: 0.5 }}>
2344
- {(!value?.name && existingFileName) &&
2422
+ {(!value?.name && existingFileName) &&
2345
2423
  <Typography>{existingFileName} selected!</Typography>
2346
2424
  }
2347
2425
  </Grid>
2348
- {error &&
2426
+ {!value && onSelectExistingFile && (
2427
+ <ExistingFilePicker
2428
+ enduserId={enduserId}
2429
+ validFileTypes={field.options?.validFileTypes}
2430
+ form={form}
2431
+ onSelect={file => {
2432
+ setError('')
2433
+ onSelectExistingFile({ secureName: file.secureName, name: file.name, type: file.type })
2434
+ }}
2435
+ />
2436
+ )}
2437
+ {error &&
2349
2438
  <Grid item alignSelf="center" sx={{ mt: 0.5 }}>
2350
2439
  <Typography color="error">{error}</Typography>
2351
2440
  </Grid>
2352
2441
  }
2353
2442
  </Grid>
2354
- )
2443
+ )
2355
2444
  }
2356
2445
 
2357
2446
  export const safe_create_url = (file: any) => {
@@ -2363,8 +2452,15 @@ export const safe_create_url = (file: any) => {
2363
2452
  }
2364
2453
  }
2365
2454
 
2366
- export const FilesInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles, form }: FormInputProps<'files'> & { existingFileName?: string }) => {
2455
+ export const FilesInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles, form, enduserId, existingSelections, onSelectExistingFile, onRemoveExistingFile }: FormInputProps<'files'> & { existingFileName?: string, existingSelections?: FormResponseAnswerFileValue[], onSelectExistingFile?: (value: FormResponseAnswerFileValue) => void, onRemoveExistingFile?: (secureName: string) => void }) => {
2367
2456
  const [error, setError] = useState('')
2457
+
2458
+ const safeExistingSelections = Array.isArray(existingSelections) ? existingSelections : undefined
2459
+
2460
+ const excludedSecureNames = useMemo(() => (
2461
+ safeExistingSelections?.map(s => s.secureName)
2462
+ ), [safeExistingSelections])
2463
+
2368
2464
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
2369
2465
  onDrop: useCallback(
2370
2466
  async acceptedFiles => {
@@ -2458,8 +2554,8 @@ export const FilesInput = ({ value, onChange, field, existingFileName, uploading
2458
2554
 
2459
2555
  {file.type?.includes('image') && previews[i] &&
2460
2556
  <Grid item>
2461
- <img
2462
- src={previews[i]!}
2557
+ <img
2558
+ src={previews[i]!}
2463
2559
  style={{ maxWidth: '45%', maxHeight: 80, height: '100%' }}
2464
2560
  />
2465
2561
  </Grid>
@@ -2476,15 +2572,44 @@ export const FilesInput = ({ value, onChange, field, existingFileName, uploading
2476
2572
  </Grid>
2477
2573
  </Grid>
2478
2574
  ))}
2575
+ {safeExistingSelections?.map((selection, i) => (
2576
+ <Grid item key={`existing-${selection.secureName}-${i}`} sx={{ mt: 0.5 }}>
2577
+ <Grid container alignItems="center" justifyContent={"space-between"} wrap="nowrap">
2578
+ <Grid item>
2579
+ <Typography sx={{ mr: 1 }}>{selection.name}</Typography>
2580
+ </Grid>
2581
+ {onRemoveExistingFile &&
2582
+ <Grid item>
2583
+ <LabeledIconButton label={form_display_text_for_language(form, "Remove")}
2584
+ Icon={Delete}
2585
+ onClick={() => onRemoveExistingFile(selection.secureName)}
2586
+ />
2587
+ </Grid>
2588
+ }
2589
+ </Grid>
2590
+ </Grid>
2591
+ ))}
2479
2592
  </Grid>
2593
+ {onSelectExistingFile && (
2594
+ <ExistingFilePicker
2595
+ enduserId={enduserId}
2596
+ excludedSecureNames={excludedSecureNames}
2597
+ validFileTypes={field.options?.validFileTypes}
2598
+ form={form}
2599
+ onSelect={file => {
2600
+ setError('')
2601
+ onSelectExistingFile({ secureName: file.secureName, name: file.name, type: file.type })
2602
+ }}
2603
+ />
2604
+ )}
2480
2605
 
2481
- {error &&
2606
+ {error &&
2482
2607
  <Grid item alignSelf="center" sx={{ mt: 0.5 }}>
2483
2608
  <Typography color="error">{error}</Typography>
2484
2609
  </Grid>
2485
2610
  }
2486
2611
  </Grid>
2487
- )
2612
+ )
2488
2613
  }
2489
2614
 
2490
2615
  const multipleChoiceItemSx: SxProps = {
@@ -5,7 +5,7 @@ import { FormInputProps } from "./types"
5
5
  import { useDropzone } from "react-dropzone"
6
6
  import { CANVAS_TITLE, EMOTII_TITLE, INSURANCE_RELATIONSHIPS, INSURANCE_RELATIONSHIPS_CANVAS, PRIMARY_HEX, RELATIONSHIP_TYPES, TELLESCOPE_GENDERS } from "@tellescope/constants"
7
7
  import { MM_DD_YYYY_to_YYYY_MM_DD, capture_is_supported, downloadFile, emit_gtm_event, first_letter_capitalized, form_response_value_to_string, getLocalTimezone, getPublicFileURL, mm_dd_yyyy, replace_enduser_template_values, responses_satisfy_conditions, truncate_string, update_local_storage, user_display_name } from "@tellescope/utilities"
8
- import { Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
8
+ import { Enduser, EnduserRelationship, FormResponseAnswerFileValue, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
9
9
  import { VALID_STATES, emailValidator, phoneValidator } from "@tellescope/validation"
10
10
  import Slider from '@mui/material/Slider';
11
11
  import LinearProgress from '@mui/material/LinearProgress';
@@ -24,6 +24,7 @@ import LanguageIcon from '@mui/icons-material/Language';
24
24
  import { CheckCircleOutline, Delete, Edit, UploadFile } from "@mui/icons-material"
25
25
  import { WYSIWYG } from "./wysiwyg"
26
26
  import { useConditionalChoices, Response, dateFromOffsetMs } from "./hooks"
27
+ import { ExistingFilePicker } from "./inputs"
27
28
 
28
29
  export const LanguageSelect = ({ value, ...props }: { value: string, onChange: (s: string) => void}) => (
29
30
  <Grid container alignItems="center" justifyContent={"center"} wrap="nowrap" spacing={1}>
@@ -930,7 +931,8 @@ export async function convertHEIC (file: FileBlob | string){
930
931
  };
931
932
 
932
933
  const value_is_image = (f?: { type?: string })=> f?.type?.includes('image')
933
- export const FileInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles, form }: FormInputProps<'file'> & { existingFileName?: string }) => {
934
+
935
+ export const FileInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles, form, enduserId, onSelectExistingFile }: FormInputProps<'file'> & { existingFileName?: string, onSelectExistingFile?: (value: FormResponseAnswerFileValue) => void }) => {
934
936
  const [error, setError] = useState('')
935
937
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
936
938
  onDrop: useCallback(
@@ -1029,17 +1031,28 @@ export const FileInput = ({ value, onChange, field, existingFileName, uploadingF
1029
1031
  </Grid>
1030
1032
 
1031
1033
  <Grid item alignSelf="center" sx={{ mt: 0.5 }}>
1032
- {(!value?.name && existingFileName) &&
1034
+ {(!value?.name && existingFileName) &&
1033
1035
  <Typography>{existingFileName} selected!</Typography>
1034
1036
  }
1035
1037
  </Grid>
1036
- {error &&
1038
+ {!value && onSelectExistingFile && (
1039
+ <ExistingFilePicker
1040
+ enduserId={enduserId}
1041
+ validFileTypes={field.options?.validFileTypes}
1042
+ form={form}
1043
+ onSelect={file => {
1044
+ setError('')
1045
+ onSelectExistingFile({ secureName: file.secureName, name: file.name, type: file.type })
1046
+ }}
1047
+ />
1048
+ )}
1049
+ {error &&
1037
1050
  <Grid item alignSelf="center" sx={{ mt: 0.5 }}>
1038
1051
  <Typography color="error">{error}</Typography>
1039
1052
  </Grid>
1040
1053
  }
1041
1054
  </Grid>
1042
- )
1055
+ )
1043
1056
  }
1044
1057
 
1045
1058
  export const safe_create_url = (file: any) => {
@@ -1051,8 +1064,15 @@ export const safe_create_url = (file: any) => {
1051
1064
  }
1052
1065
  }
1053
1066
 
1054
- export const FilesInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles, form }: FormInputProps<'files'> & { existingFileName?: string }) => {
1067
+ export const FilesInput = ({ value, onChange, field, existingFileName, uploadingFiles, handleFileUpload, setUploadingFiles, form, enduserId, existingSelections, onSelectExistingFile, onRemoveExistingFile }: FormInputProps<'files'> & { existingFileName?: string, existingSelections?: FormResponseAnswerFileValue[], onSelectExistingFile?: (value: FormResponseAnswerFileValue) => void, onRemoveExistingFile?: (secureName: string) => void }) => {
1055
1068
  const [error, setError] = useState('')
1069
+
1070
+ const safeExistingSelections = Array.isArray(existingSelections) ? existingSelections : undefined
1071
+
1072
+ const excludedSecureNames = useMemo(() => (
1073
+ safeExistingSelections?.map(s => s.secureName)
1074
+ ), [safeExistingSelections])
1075
+
1056
1076
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
1057
1077
  onDrop: useCallback(
1058
1078
  async acceptedFiles => {
@@ -1164,15 +1184,45 @@ export const FilesInput = ({ value, onChange, field, existingFileName, uploading
1164
1184
  </Grid>
1165
1185
  </Grid>
1166
1186
  ))}
1187
+ {existingSelections?.map((selection, i) => (
1188
+ <Grid item key={`existing-${selection.secureName}-${i}`} sx={{ mt: 0.5 }}>
1189
+ <Grid container alignItems="center" justifyContent={"space-between"} wrap="nowrap">
1190
+ <Grid item>
1191
+ <Typography sx={{ mr: 1 }}>{selection.name}</Typography>
1192
+ </Grid>
1193
+ {onRemoveExistingFile &&
1194
+ <Grid item>
1195
+ <LabeledIconButton label={form_display_text_for_language(form, "Remove")}
1196
+ Icon={Delete}
1197
+ onClick={() => onRemoveExistingFile(selection.secureName)}
1198
+ />
1199
+ </Grid>
1200
+ }
1201
+ </Grid>
1202
+ </Grid>
1203
+ ))}
1167
1204
  </Grid>
1168
1205
 
1169
- {error &&
1206
+ {onSelectExistingFile && (
1207
+ <ExistingFilePicker
1208
+ enduserId={enduserId}
1209
+ excludedSecureNames={excludedSecureNames}
1210
+ validFileTypes={field.options?.validFileTypes}
1211
+ form={form}
1212
+ onSelect={file => {
1213
+ setError('')
1214
+ onSelectExistingFile({ secureName: file.secureName, name: file.name, type: file.type })
1215
+ }}
1216
+ />
1217
+ )}
1218
+
1219
+ {error &&
1170
1220
  <Grid item alignSelf="center" sx={{ mt: 0.5 }}>
1171
1221
  <Typography color="error">{error}</Typography>
1172
1222
  </Grid>
1173
1223
  }
1174
1224
  </Grid>
1175
- )
1225
+ )
1176
1226
  }
1177
1227
 
1178
1228
  export const MultipleChoiceInput = ({ field, form, value: _value, onChange, responses, enduser }: FormInputProps<'multiple_choice'>) => {