@tellescope/react-components 1.247.0 → 1.249.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 (81) hide show
  1. package/lib/cjs/Forms/form_responses.d.ts +11 -0
  2. package/lib/cjs/Forms/form_responses.d.ts.map +1 -1
  3. package/lib/cjs/Forms/form_responses.js +38 -2
  4. package/lib/cjs/Forms/form_responses.js.map +1 -1
  5. package/lib/cjs/Forms/forms.d.ts +4 -3
  6. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  7. package/lib/cjs/Forms/forms.js +41 -15
  8. package/lib/cjs/Forms/forms.js.map +1 -1
  9. package/lib/cjs/Forms/forms.v2.d.ts +2 -2
  10. package/lib/cjs/Forms/forms.v2.d.ts.map +1 -1
  11. package/lib/cjs/Forms/forms.v2.js +8 -8
  12. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  13. package/lib/cjs/Forms/hooks.d.ts +2 -0
  14. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  15. package/lib/cjs/Forms/hooks.js +57 -16
  16. package/lib/cjs/Forms/hooks.js.map +1 -1
  17. package/lib/cjs/Forms/inputs.d.ts +1 -1
  18. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  19. package/lib/cjs/Forms/inputs.js +6 -3
  20. package/lib/cjs/Forms/inputs.js.map +1 -1
  21. package/lib/cjs/Forms/inputs.v2.d.ts +1 -2
  22. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  23. package/lib/cjs/Forms/inputs.v2.js +1 -44
  24. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  25. package/lib/cjs/Forms/types.d.ts +1 -0
  26. package/lib/cjs/Forms/types.d.ts.map +1 -1
  27. package/lib/cjs/state.d.ts +7 -1
  28. package/lib/cjs/state.d.ts.map +1 -1
  29. package/lib/cjs/state.js +43 -1
  30. package/lib/cjs/state.js.map +1 -1
  31. package/lib/esm/CMS/components.d.ts +1 -0
  32. package/lib/esm/CMS/components.d.ts.map +1 -1
  33. package/lib/esm/Forms/form_responses.d.ts +11 -0
  34. package/lib/esm/Forms/form_responses.d.ts.map +1 -1
  35. package/lib/esm/Forms/form_responses.js +37 -2
  36. package/lib/esm/Forms/form_responses.js.map +1 -1
  37. package/lib/esm/Forms/forms.d.ts +6 -5
  38. package/lib/esm/Forms/forms.d.ts.map +1 -1
  39. package/lib/esm/Forms/forms.js +41 -15
  40. package/lib/esm/Forms/forms.js.map +1 -1
  41. package/lib/esm/Forms/forms.v2.d.ts +4 -4
  42. package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
  43. package/lib/esm/Forms/forms.v2.js +8 -8
  44. package/lib/esm/Forms/forms.v2.js.map +1 -1
  45. package/lib/esm/Forms/hooks.d.ts +2 -0
  46. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  47. package/lib/esm/Forms/hooks.js +58 -17
  48. package/lib/esm/Forms/hooks.js.map +1 -1
  49. package/lib/esm/Forms/inputs.d.ts +3 -3
  50. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  51. package/lib/esm/Forms/inputs.js +6 -3
  52. package/lib/esm/Forms/inputs.js.map +1 -1
  53. package/lib/esm/Forms/inputs.native.d.ts +1 -0
  54. package/lib/esm/Forms/inputs.native.d.ts.map +1 -1
  55. package/lib/esm/Forms/inputs.v2.d.ts +1 -2
  56. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  57. package/lib/esm/Forms/inputs.v2.js +1 -44
  58. package/lib/esm/Forms/inputs.v2.js.map +1 -1
  59. package/lib/esm/Forms/types.d.ts +1 -0
  60. package/lib/esm/Forms/types.d.ts.map +1 -1
  61. package/lib/esm/TwilioVideo/hooks.d.ts +1 -1
  62. package/lib/esm/controls.d.ts +2 -2
  63. package/lib/esm/inputs.d.ts +1 -1
  64. package/lib/esm/inputs.native.d.ts +1 -0
  65. package/lib/esm/inputs.native.d.ts.map +1 -1
  66. package/lib/esm/state.d.ts +336 -330
  67. package/lib/esm/state.d.ts.map +1 -1
  68. package/lib/esm/state.js +42 -1
  69. package/lib/esm/state.js.map +1 -1
  70. package/lib/esm/theme.native.d.ts +1 -0
  71. package/lib/esm/theme.native.d.ts.map +1 -1
  72. package/lib/tsconfig.tsbuildinfo +1 -1
  73. package/package.json +9 -9
  74. package/src/Forms/form_responses.tsx +133 -2
  75. package/src/Forms/forms.tsx +52 -20
  76. package/src/Forms/forms.v2.tsx +13 -7
  77. package/src/Forms/hooks.tsx +67 -14
  78. package/src/Forms/inputs.tsx +6 -3
  79. package/src/Forms/inputs.v2.tsx +1 -50
  80. package/src/Forms/types.ts +1 -0
  81. package/src/state.tsx +37 -0
@@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
2
2
  import { Session } from "@tellescope/sdk"
3
3
  import { ChangeHandler, FormFieldNode } from "./types"
4
4
  import { DatabaseRecord, Enduser, Form, FormField, FormResponse } from "@tellescope/types-client"
5
- import { phoneValidator } from "@tellescope/validation"
5
+ import { phoneValidator, is_valid_mm_dd_yyyy } from "@tellescope/validation"
6
6
  import { FileBlob, Indexable } from "@tellescope/types-utilities"
7
7
  import { CompoundFilter, EnduserRelationship, FormCustomization, FormFieldOptionDetails, FormResponseAnswerAddress, FormResponseAnswerFileValue, FormResponseValue, FormResponseValueAnswer, OrganizationTheme, PreviousFormCompoundLogic, PreviousFormFieldType, Timezone, TIMEZONES } from "@tellescope/types-models"
8
8
  import { WithTheme, contact_is_valid, useAddGTMTag, useFileUpload, useFormFields, useFormResponses, useResolvedSession, value_is_loaded } from "../index"
@@ -425,6 +425,7 @@ export const useOrganizationTheme = () => {
425
425
  return context?.theme ?? theme
426
426
  }
427
427
 
428
+ /** @deprecated Use is_valid_mm_dd_yyyy from @tellescope/validation instead — it validates days-in-month and leap years */
428
429
  export const isDateString = (_s='') => {
429
430
  const s = _s.trim()
430
431
 
@@ -442,7 +443,7 @@ export const isDateString = (_s='') => {
442
443
  // const [mm,dd,yyyy] = s.split('-').map(v => parseInt(v)) // don't shorthand, for radix argument of parseInt gets messed up
443
444
  // const d = Date.parse(`${yyyy}-${mm}-${dd}`) // this format should be explicitly supported by all implementations
444
445
  // if (isNaN(d)) return false
445
-
446
+
446
447
  return true
447
448
  }
448
449
  const isZIPString = (s='') => /^\d{5}$/.test(s) || /^\d{5}-\d{4}$/.test(s)
@@ -500,6 +501,41 @@ const existing_response_if_compatible = (existingResponses: FormResponseValue[]
500
501
  return undefined // no valid match, write off as data loss due to incompatible type of new question
501
502
  }
502
503
 
504
+ // Filter stale multiple-choice/dropdown selections whose options are now hidden by showCondition
505
+ const filter_stale_choices = (
506
+ value: string[] | undefined,
507
+ field: FormField,
508
+ responseContext: FormResponseValue[],
509
+ enduser?: Partial<Enduser>,
510
+ form?: Form,
511
+ ): string[] | undefined => {
512
+ if (!value || !Array.isArray(value)) return value
513
+ if (field.type !== 'multiple_choice' && field.type !== 'Dropdown') return value
514
+
515
+ const options = field.options as { choices?: string[], optionDetails?: FormFieldOptionDetails[] } | undefined
516
+ if (!options?.optionDetails?.length) return value // no option-level conditions to check
517
+
518
+ const choices = options.choices ?? []
519
+
520
+ return value.filter(v => {
521
+ // preserve values not in the choices array (e.g. "other" free-text)
522
+ if (!choices.includes(v)) return true
523
+
524
+ const optionDetail = options.optionDetails?.find(d => d.option === v)
525
+ if (!optionDetail?.showCondition || object_is_empty(optionDetail.showCondition)) {
526
+ return true // no condition means always visible
527
+ }
528
+
529
+ return responses_satisfy_conditions(responseContext, optionDetail.showCondition, {
530
+ dateOfBirth: enduser?.dateOfBirth,
531
+ gender: enduser?.gender,
532
+ state: enduser?.state,
533
+ form,
534
+ activeResponses: responseContext,
535
+ })
536
+ })
537
+ }
538
+
503
539
  const shouldCallout = (field: FormField | undefined, value: FormResponseValueAnswer['value']) => {
504
540
  if (!field) return false
505
541
  if (!value) return false
@@ -545,6 +581,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
545
581
  const [currentPageIndex, setCurrentPageIndex] = useState(0)
546
582
  const [uploadingFiles, setUploadingFiles] = useState<{ fieldId: string }[]>([])
547
583
  const prevFieldStackRef = useRef<typeof root[]>([])
584
+ const lastNavigationDirectionRef = useRef<'forward' | 'backward' | null>(null)
548
585
 
549
586
  // Auto-advance state for form continuation
550
587
  const [isAutoAdvancing, setIsAutoAdvancing] = useState(false)
@@ -653,14 +690,22 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
653
690
  // placeholders for initial fields, reset when fields prop changes, since questions are now different (e.g. different form selected)
654
691
  const fieldInitRef = useRef('')
655
692
  const initializeFields = useCallback(() => (
656
- fields.map(f => ({
693
+ fields.map(f => {
694
+ const existingValue = existing_response_if_compatible(existingResponses, f)
695
+ const filteredValue = (
696
+ existingValue != null && Array.isArray(existingValue) && (f.type === 'multiple_choice' || f.type === 'Dropdown')
697
+ ? filter_stale_choices(existingValue as string[], f, existingResponses ?? [], enduser, form)
698
+ : existingValue
699
+ )
700
+
701
+ return {
657
702
  fieldId: f.id,
658
703
  fieldTitle: f.title,
659
704
  fieldDescription: f.description,
660
705
  fieldHtmlDescription: f.htmlDescription,
661
706
  externalId: f.externalId,
662
707
  intakeField: f.intakeField || undefined,
663
- touched: false,
708
+ touched: false,
664
709
  includeInSubmit: false,
665
710
  sharedWithEnduser: f.sharedWithEnduser,
666
711
  isCalledOut: existingResponses?.find(r => r.fieldId === f.id)?.isCalledOut,
@@ -680,11 +725,11 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
680
725
  : f?.intakeField === 'Address' && existingResponses?.find(r => r.fieldId === f.id && r.answer.type === 'Address')
681
726
  ? 'State'
682
727
  : undefined
683
- ) as any,
684
- answer: {
728
+ ) as any,
729
+ answer: {
685
730
  type: f.type,
686
731
  value: (
687
- existing_response_if_compatible(existingResponses, f) ?? (
732
+ filteredValue ?? (
688
733
  (f.type === 'Insurance' || f.type === 'Address' || f.type === 'file' || f.type === 'signature' || f.type === 'multiple_choice' || f.type === 'Dropdown' || f.type === 'Table Input' || f.type === 'Database Select' || f.type === 'Medications' || f.type === 'Pharmacy Search')
689
734
  ? undefined
690
735
  : f.type === 'Question Group'
@@ -710,8 +755,8 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
710
755
  ),
711
756
  } as FormResponseValueAnswer,
712
757
  field: f,
713
- }))
714
- ), [fields, existingResponses])
758
+ }})
759
+ ), [fields, existingResponses, enduser, form])
715
760
 
716
761
  const [responses, setResponses] = useState<(Response)[]>(initializeFields())
717
762
  useEffect(() => {
@@ -993,7 +1038,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
993
1038
  }
994
1039
 
995
1040
  if (value.answer.type === 'Insurance') {
996
- if (value.answer.value?.relationshipDetails?.dateOfBirth && !isDateString(value.answer.value.relationshipDetails.dateOfBirth)) {
1041
+ if (value.answer.value?.relationshipDetails?.dateOfBirth && !is_valid_mm_dd_yyyy(value.answer.value.relationshipDetails.dateOfBirth)) {
997
1042
  return "Enter date of birth in MM-DD-YYYY format"
998
1043
  }
999
1044
  if (field.isOptional) return null
@@ -1240,7 +1285,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1240
1285
  }
1241
1286
  }
1242
1287
  } else if (value.answer.type === 'dateString') {
1243
- if (!isDateString(value.answer.value)) {
1288
+ if (!is_valid_mm_dd_yyyy(value.answer.value)) {
1244
1289
  return "Enter a date in MM-DD-YYYY format"
1245
1290
  }
1246
1291
  } else if (value.answer.type === 'multiple_choice' || value.answer.type === 'Dropdown') {
@@ -1291,7 +1336,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1291
1336
  for (const row of value.answer.value ?? []) {
1292
1337
  for (const cell of row) {
1293
1338
  const type = field.options?.tableChoices?.find(t => t.label === cell.label)?.type
1294
- if (type === 'Date' && !isDateString(cell.entry)) {
1339
+ if (type === 'Date' && !is_valid_mm_dd_yyyy(cell.entry)) {
1295
1340
  return `Enter a date in MM-DD-YYYY format for ${cell.label} in row ${(value.answer.value?.indexOf(row) ?? 0) + 1}`
1296
1341
  }
1297
1342
  }
@@ -1446,12 +1491,14 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1446
1491
  responses: [
1447
1492
  ...responsesToSubmit,
1448
1493
  // include existing responses in case previously saved as draft
1449
- ...(existingResponses ?? []).filter(r =>
1494
+ ...(existingResponses ?? []).filter(r =>
1450
1495
  !responsesToSubmit.find(_r => r.fieldId === _r.fieldId)
1451
1496
  // but don't include responses which were populated from a patient field and not a prior response
1452
1497
  // if these are edited, they would be included in responsesToSubmit
1453
1498
  && !r.isPrepopulatedFromEnduserField
1454
- ),
1499
+ )
1500
+ // initializeFields leverages filter_stale_choices to strip answers whose options are no longer visible in multiple choice type questions
1501
+ // existingResponses may still carry stale values for fields the user didn't interact with this session, but preserving them as-is avoids unexpected data loss
1455
1502
  ],
1456
1503
  automationStepId,
1457
1504
  customerId,
@@ -1548,6 +1595,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1548
1595
  if (!currentValue) return
1549
1596
  if (isNextDisabled() && currentValue?.answer.type !== 'Hidden Value') return
1550
1597
 
1598
+ lastNavigationDirectionRef.current = 'forward'
1551
1599
  console.log('going to next field')
1552
1600
  if (currentValue.answer.type === 'Question Group') {
1553
1601
  const responsesToSave = (
@@ -1612,6 +1660,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1612
1660
  const goToPreviousField = useCallback(() => {
1613
1661
  if (isPreviousDisabled()) return
1614
1662
 
1663
+ lastNavigationDirectionRef.current = 'backward'
1615
1664
  updateInclusion(false)
1616
1665
 
1617
1666
  const previous = prevFieldStackRef.current.pop()
@@ -1634,6 +1683,9 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1634
1683
  touched,
1635
1684
  isCalledOut: shouldCallout(fields?.find(f => f?.id === fieldId), value),
1636
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 } : {}),
1637
1689
  answer: {
1638
1690
  ...r.answer,
1639
1691
  value: value as any,
@@ -1745,6 +1797,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1745
1797
  uploadingFiles, setUploadingFiles,
1746
1798
  handleFileUpload,
1747
1799
  isAutoAdvancing,
1800
+ lastNavigationDirectionRef,
1748
1801
  }
1749
1802
  }
1750
1803
 
@@ -4895,7 +4895,7 @@ export const RedirectInput = ({ enduserId, groupId, groupInsance, rootResponseId
4895
4895
  return null
4896
4896
  }
4897
4897
 
4898
- export const HiddenValueInput = ({ goToNextField, goToPreviousField, field, value, onChange, isSinglePage, groupFields }: FormInputProps<'email'>) => {
4898
+ export const HiddenValueInput = ({ goToNextField, goToPreviousField, field, value, onChange, isSinglePage, groupFields, lastNavigationDirectionRef }: FormInputProps<'email'>) => {
4899
4899
  let lastRef = useRef(0)
4900
4900
  let lastIdRef = useRef('')
4901
4901
 
@@ -4925,7 +4925,10 @@ export const HiddenValueInput = ({ goToNextField, goToPreviousField, field, valu
4925
4925
  lastRef.current = Date.now()
4926
4926
  lastIdRef.current = field.id
4927
4927
 
4928
- if (value) {
4928
+ // Only collapse backward through a chain of hidden fields when actually navigating backward.
4929
+ // On forward nav (including form resume where existingResponses pre-populate the value),
4930
+ // fall through to the else branch so we re-set and advance instead of bouncing back.
4931
+ if (value && lastNavigationDirectionRef?.current === 'backward') {
4929
4932
  if (isSinglePage) return
4930
4933
  onChange('', field.id)
4931
4934
 
@@ -4939,7 +4942,7 @@ export const HiddenValueInput = ({ goToNextField, goToPreviousField, field, valu
4939
4942
  // pass value that is set after above onChange
4940
4943
  goToNextField?.({ type: 'Hidden Value', value: valueToSet })
4941
4944
  }
4942
- }, [value, onChange, field.id, valueToSet, goToNextField, goToPreviousField, isSinglePage, dontNavigate])
4945
+ }, [value, onChange, field.id, valueToSet, goToNextField, goToPreviousField, isSinglePage, dontNavigate, lastNavigationDirectionRef])
4943
4946
 
4944
4947
  return <></>
4945
4948
  }
@@ -2371,56 +2371,7 @@ export const HeightInput = ({ field, value={} as any, onChange, form, ...props }
2371
2371
  )
2372
2372
 
2373
2373
  // Re-export from V1 to follow DRY principles
2374
- export { RedirectInput } from './inputs'
2375
-
2376
- export const HiddenValueInput = ({ goToNextField, goToPreviousField, field, value, onChange, isSinglePage, groupFields }: FormInputProps<'email'>) => {
2377
- let lastRef = useRef(0)
2378
- let lastIdRef = useRef('')
2379
-
2380
- // in a Question Group, only the first Hidden Value should navigate
2381
- // AND, it should only navigate if the group only contains hidden values
2382
- const firstHiddenValue = groupFields?.find(v => v.type === 'Hidden Value')
2383
- const dontNavigate = (
2384
- (firstHiddenValue && firstHiddenValue?.id !== field.id) // is in a group, but not the first hidden value
2385
- || !!(groupFields?.find(v => v.type !== 'Hidden Value')) // group contains at least 1 non-hidden value
2386
- )
2387
-
2388
- const publicIdentifier = useMemo(() => {
2389
- try {
2390
- return new URL(window.location.href).searchParams.get('publicIdentifier') || ''
2391
- } catch(err) {
2392
- return ''
2393
- }
2394
- }, [])
2395
-
2396
- const valueToSet = useMemo(() => (
2397
- (field.title === "{{PUBLIC_IDENTIFIER}}" && publicIdentifier) ? publicIdentifier
2398
- : field.title
2399
- ), [field.title, publicIdentifier])
2400
-
2401
- useEffect(() => {
2402
- if (lastRef.current > Date.now() - 1000 && lastIdRef.current === field.id) return
2403
- lastRef.current = Date.now()
2404
- lastIdRef.current = field.id
2405
-
2406
- if (value) {
2407
- if (isSinglePage) return
2408
- onChange('', field.id)
2409
-
2410
- if (dontNavigate) return
2411
- goToPreviousField?.()
2412
- } else {
2413
- onChange(valueToSet, field.id)
2414
-
2415
- if (dontNavigate) return
2416
-
2417
- // pass value that is set after above onChange
2418
- goToNextField?.({ type: 'Hidden Value', value: valueToSet })
2419
- }
2420
- }, [value, onChange, field.id, valueToSet, goToNextField, goToPreviousField, isSinglePage, dontNavigate])
2421
-
2422
- return <></>
2423
- }
2374
+ export { RedirectInput, HiddenValueInput } from './inputs'
2424
2375
 
2425
2376
  export const EmotiiInput = ({ goToNextField, goToPreviousField, field, value, onChange, form, formResponseId, ...props }: FormInputProps<'email'>) => {
2426
2377
  const session = useResolvedSession()
@@ -39,6 +39,7 @@ export interface FormInputProps<K extends keyof AnswerForType> {
39
39
  setUploadingFiles?: React.Dispatch<React.SetStateAction<{ fieldId: string }[]>>,
40
40
  groupFields?: FormField[],
41
41
  inputProps?: { sx: SxProps },
42
+ lastNavigationDirectionRef?: React.MutableRefObject<'forward' | 'backward' | null>,
42
43
  }
43
44
 
44
45
  export type FormInputs = {
package/src/state.tsx CHANGED
@@ -2637,6 +2637,43 @@ export const useIntegrations = (options={} as HookOptions<Integration>) => {
2637
2637
  )
2638
2638
  }
2639
2639
 
2640
+ export const useRedactedIntegrations = () => {
2641
+ const session = useSession()
2642
+ const [loadingState, setLoadingState] = useState<LoadedData<Integration[]>>({ status: LoadingStatus.Fetching, value: [] as any })
2643
+ const fetchedRef = useRef(false)
2644
+
2645
+ useEffect(() => {
2646
+ if (fetchedRef.current) return
2647
+ fetchedRef.current = true
2648
+
2649
+ let cancelled = false
2650
+ session.api.integrations.load_redacted({})
2651
+ .then((result: any) => {
2652
+ if (!cancelled) setLoadingState({ status: LoadingStatus.Loaded, value: result.integrations })
2653
+ })
2654
+ .catch((err: any) => {
2655
+ if (!cancelled) setLoadingState({ status: LoadingStatus.Error, value: err })
2656
+ })
2657
+ return () => { cancelled = true }
2658
+ }, [session])
2659
+
2660
+ const updateIntegration = useCallback(async (id: string, updates: Partial<Integration>) => {
2661
+ const result: any = await session.api.integrations.update_settings({ id, updates })
2662
+ setLoadingState((prev: LoadedData<Integration[]>) => {
2663
+ if (prev.status !== LoadingStatus.Loaded) return prev
2664
+ return {
2665
+ ...prev,
2666
+ value: prev.value.map((i: Integration) => i.id === id ? { ...i, ...result.integration } : i),
2667
+ }
2668
+ })
2669
+ }, [session])
2670
+
2671
+ return [
2672
+ loadingState,
2673
+ { updateIntegration },
2674
+ ] as const
2675
+ }
2676
+
2640
2677
  export const usePortalCustomizations = (options={} as HookOptions<PortalCustomization>) => {
2641
2678
  const session = useResolvedSession()
2642
2679
  return useListStateHook(