@tellescope/react-components 1.231.0 → 1.232.1

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 (50) hide show
  1. package/lib/cjs/Forms/forms.js +1 -1
  2. package/lib/cjs/Forms/forms.js.map +1 -1
  3. package/lib/cjs/Forms/forms.v2.d.ts +1 -1
  4. package/lib/cjs/Forms/forms.v2.js +1 -1
  5. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  6. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  7. package/lib/cjs/Forms/hooks.js +24 -0
  8. package/lib/cjs/Forms/hooks.js.map +1 -1
  9. package/lib/cjs/Forms/inputs.d.ts +6 -3
  10. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  11. package/lib/cjs/Forms/inputs.js +171 -44
  12. package/lib/cjs/Forms/inputs.js.map +1 -1
  13. package/lib/cjs/Forms/inputs.v2.d.ts +7 -11
  14. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  15. package/lib/cjs/Forms/inputs.v2.js +16 -445
  16. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  17. package/lib/esm/CMS/components.d.ts +0 -1
  18. package/lib/esm/CMS/components.d.ts.map +1 -1
  19. package/lib/esm/Forms/form_responses.d.ts +0 -1
  20. package/lib/esm/Forms/form_responses.d.ts.map +1 -1
  21. package/lib/esm/Forms/forms.d.ts +3 -3
  22. package/lib/esm/Forms/forms.js +1 -1
  23. package/lib/esm/Forms/forms.js.map +1 -1
  24. package/lib/esm/Forms/forms.v2.d.ts +4 -4
  25. package/lib/esm/Forms/forms.v2.js +1 -1
  26. package/lib/esm/Forms/forms.v2.js.map +1 -1
  27. package/lib/esm/Forms/hooks.d.ts +0 -1
  28. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  29. package/lib/esm/Forms/hooks.js +24 -0
  30. package/lib/esm/Forms/hooks.js.map +1 -1
  31. package/lib/esm/Forms/inputs.d.ts +7 -4
  32. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  33. package/lib/esm/Forms/inputs.js +173 -46
  34. package/lib/esm/Forms/inputs.js.map +1 -1
  35. package/lib/esm/Forms/inputs.v2.d.ts +8 -12
  36. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  37. package/lib/esm/Forms/inputs.v2.js +17 -446
  38. package/lib/esm/Forms/inputs.v2.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/state.d.ts +315 -315
  44. package/lib/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +9 -9
  46. package/src/Forms/forms.tsx +1 -1
  47. package/src/Forms/forms.v2.tsx +1 -1
  48. package/src/Forms/hooks.tsx +33 -5
  49. package/src/Forms/inputs.tsx +224 -35
  50. package/src/Forms/inputs.v2.tsx +20 -639
@@ -1,11 +1,11 @@
1
1
  import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"
2
2
  import axios from "axios"
3
- import { Autocomplete, Box, Button, Checkbox, Chip, Divider, FormControl, FormControlLabel, FormLabel, Grid, InputLabel, MenuItem, Radio, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
3
+ import { Autocomplete, Box, Button, Checkbox, Chip, Divider, FormControl, Grid, InputLabel, MenuItem, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
4
4
  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
- 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, replace_enduser_template_values, truncate_string, update_local_storage, user_display_name } from "@tellescope/utilities"
8
- import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, FormFieldOptionDetails, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
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 { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, 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';
@@ -21,8 +21,6 @@ import heic2any from "heic2any"
21
21
  import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
22
22
  import LanguageIcon from '@mui/icons-material/Language';
23
23
 
24
- import { Elements, PaymentElement, useStripe, useElements, EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js';
25
- import { loadStripe } from '@stripe/stripe-js';
26
24
  import { CheckCircleOutline, Delete, Edit, UploadFile } from "@mui/icons-material"
27
25
  import { WYSIWYG } from "./wysiwyg"
28
26
 
@@ -1666,339 +1664,12 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1666
1664
  )
1667
1665
  }
1668
1666
 
1669
- export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }: FormInputProps<'Stripe'> & {
1670
- setCustomerId: React.Dispatch<React.SetStateAction<string | undefined>>,
1671
- }) => {
1672
- const session = useResolvedSession()
1673
- const [clientSecret, setClientSecret] = useState('')
1674
- const [businessName, setBusinessName] = useState('')
1675
- const [isCheckout, setIsCheckout] = useState(false)
1676
- const [stripePromise, setStripePromise] = useState<ReturnType<typeof loadStripe>>()
1677
- const [answertext, setAnswertext] = useState('')
1678
- const [error, setError] = useState('')
1679
- const [selectedProducts, setSelectedProducts] = useState<string[]>([])
1680
- const [showProductSelection, setShowProductSelection] = useState(false)
1681
- const [availableProducts, setAvailableProducts] = useState<any[]>([])
1682
- const [loadingProducts, setLoadingProducts] = useState(false)
1683
-
1684
- const fetchRef = useRef(false)
1685
- useEffect(() => {
1686
- if (fetchRef.current) return
1687
- if (value && (session.userInfo as any)?.stripeCustomerId) {
1688
- return setCustomerId(c => c ? c : (session.userInfo as any)?.stripeCustomerId) // already paid or saved card
1689
- }
1690
-
1691
- // Check if product selection mode is enabled
1692
- if (field.options?.stripeProductSelectionMode && (field.options?.productIds || []).length > 1) {
1693
- setShowProductSelection(true)
1694
- setLoadingProducts(true)
1695
-
1696
- // Fetch product data with real-time Stripe pricing via proxy_read
1697
- const productIds = (field.options.productIds || []).join(',')
1698
- session.api.integrations.proxy_read({
1699
- integration: 'Stripe',
1700
- type: 'product-prices',
1701
- id: productIds,
1702
- query: field.options.stripeKey
1703
- })
1704
- .then(({ data }) => {
1705
- setAvailableProducts(data.products || [])
1706
- setLoadingProducts(false)
1707
- })
1708
- .catch((e: any) => {
1709
- console.error('Error loading product data:', e)
1710
- const errorMessage = e?.message?.includes?.('Stripe pricing error:')
1711
- ? e.message.replace('Stripe pricing error: ', '')
1712
- : 'Failed to load product information from Stripe'
1713
- setError(`Product configuration error: ${errorMessage}`)
1714
- setLoadingProducts(false)
1715
- })
1716
- return
1717
- }
1718
-
1719
- fetchRef.current = true
1720
-
1721
- session.api.form_responses.stripe_details({ fieldId: field.id, enduserId })
1722
- .then(({ clientSecret, publishableKey, stripeAccount, businessName, customerId, isCheckout, answerText }) => {
1723
- setAnswertext(answerText || '')
1724
- setIsCheckout(!!isCheckout)
1725
- setClientSecret(clientSecret)
1726
- setStripePromise(loadStripe(publishableKey, { stripeAccount }))
1727
- setBusinessName(businessName)
1728
- setCustomerId(customerId)
1729
- })
1730
- .catch((e: any) => {
1731
- console.error(e)
1732
- if (typeof e?.message === 'string') {
1733
- setError(e.message)
1734
- }
1735
- })
1736
- }, [session, value, field.id, enduserId])
1737
-
1738
- const cost = (
1739
- showProductSelection
1740
- ? selectedProducts.reduce((total, productId) => {
1741
- const product = availableProducts.find(p => p._id === productId)
1742
- if (product?.currentPrice) {
1743
- return total + (product.currentPrice.amount || 0)
1744
- }
1745
- return total + (product?.cost?.amount || 0)
1746
- }, 0)
1747
- : 0 // Will be calculated by existing Stripe flow when not in selection mode
1748
- )
1749
-
1750
- // Handle product selection step
1751
- if (showProductSelection) {
1752
- if (error) {
1753
- return (
1754
- <Grid container direction="column" spacing={2} alignItems="center">
1755
- <Grid item>
1756
- <Typography color="error" variant="h6">
1757
- Product Configuration Error
1758
- </Typography>
1759
- </Grid>
1760
- <Grid item>
1761
- <Typography color="error" sx={{ textAlign: 'center' }}>
1762
- {error}
1763
- </Typography>
1764
- </Grid>
1765
- </Grid>
1766
- )
1767
- }
1768
-
1769
- if (loadingProducts) {
1770
- return (
1771
- <Grid container direction="column" spacing={2} alignItems="center">
1772
- <Grid item>
1773
- <LinearProgress />
1774
- </Grid>
1775
- <Grid item>
1776
- <Typography>Loading product information...</Typography>
1777
- </Grid>
1778
- </Grid>
1779
- )
1780
- }
1781
- const isSingleSelection = field.options?.radio === true
1782
-
1783
- const handleProductSelection = (productId: string) => {
1784
- if (isSingleSelection) {
1785
- setSelectedProducts([productId])
1786
- } else {
1787
- setSelectedProducts(prev =>
1788
- prev.includes(productId)
1789
- ? prev.filter(id => id !== productId)
1790
- : [...prev, productId]
1791
- )
1792
- }
1793
- }
1794
-
1795
- const handleContinueToPayment = () => {
1796
- if (selectedProducts.length === 0) return
1797
- setShowProductSelection(false)
1798
- fetchRef.current = true
1799
-
1800
- // Now fetch Stripe details with selected products
1801
- session.api.form_responses.stripe_details({
1802
- fieldId: field.id,
1803
- enduserId,
1804
- ...(selectedProducts.length > 0 && { selectedProductIds: selectedProducts }) // Pass selected products to Stripe checkout
1805
- } as any)
1806
- .then(({ clientSecret, publishableKey, stripeAccount, businessName, customerId, isCheckout, answerText }) => {
1807
- setAnswertext(answerText || '')
1808
- setIsCheckout(!!isCheckout)
1809
- setClientSecret(clientSecret)
1810
- setStripePromise(loadStripe(publishableKey, { stripeAccount }))
1811
- setBusinessName(businessName)
1812
- setCustomerId(customerId)
1813
- })
1814
- .catch((e: any) => {
1815
- console.error(e)
1816
- if (typeof e?.message === 'string') {
1817
- setError(e.message)
1818
- }
1819
- })
1820
- }
1821
-
1822
- return (
1823
- <Grid container direction="column" spacing={2}>
1824
- <Grid item>
1825
- <Typography variant="h6">Select Product{isSingleSelection ? '' : 's'}</Typography>
1826
- </Grid>
1827
-
1828
- {availableProducts.map((product) => {
1829
- // Use real-time Stripe pricing if available, fallback to Tellescope pricing
1830
- const price = product.currentPrice || product.cost
1831
- const priceAmount = price?.amount || 0
1832
- const priceCurrency = price?.currency || 'USD'
1833
-
1834
- return (
1835
- <Grid item key={product._id}>
1836
- <FormControlLabel
1837
- control={
1838
- isSingleSelection ? (
1839
- <Radio
1840
- checked={selectedProducts.includes(product._id)}
1841
- onChange={() => handleProductSelection(product._id)}
1842
- />
1843
- ) : (
1844
- <Checkbox
1845
- checked={selectedProducts.includes(product._id)}
1846
- onChange={() => handleProductSelection(product._id)}
1847
- />
1848
- )
1849
- }
1850
- label={
1851
- <Box>
1852
- <Typography variant="body1" fontWeight="bold">
1853
- {product.title}
1854
- </Typography>
1855
- {product.description && (
1856
- <Typography variant="body2" color="textSecondary">
1857
- {product.description}
1858
- </Typography>
1859
- )}
1860
- <Typography variant="body2" color="primary">
1861
- ${(priceAmount / 100).toFixed(2)} {priceCurrency.toUpperCase()}
1862
- {product.currentPrice?.isSubscription && (
1863
- <Typography component="span" variant="caption" sx={{ ml: 0.5 }}>
1864
- {format_stripe_subscription_interval(product.currentPrice?.interval, product.currentPrice?.interval_count)}
1865
- </Typography>
1866
- )}
1867
- </Typography>
1868
- </Box>
1869
- }
1870
- />
1871
- </Grid>
1872
- )
1873
- })}
1874
-
1875
- <Grid item>
1876
- <Button
1877
- variant="contained"
1878
- onClick={handleContinueToPayment}
1879
- disabled={selectedProducts.length === 0}
1880
- sx={{ mt: 2 }}
1881
- >
1882
- Continue to Payment
1883
- </Button>
1884
- </Grid>
1885
- </Grid>
1886
- )
1887
- }
1888
-
1889
- if (error) {
1890
- return (
1891
- <Typography color="error">
1892
- {error}
1893
- </Typography>
1894
- )
1895
- }
1896
- if (value) {
1897
- return (
1898
- <Grid container alignItems="center" wrap="nowrap">
1899
- <CheckCircleOutline color="success" />
1900
-
1901
- <Typography sx={{ ml: 1, fontSize: 20 }}>
1902
- {field.options?.chargeImmediately ? 'Your purchase was successful' : "Your payment details have been saved!"}
1903
- </Typography>
1904
- </Grid>
1905
- )
1906
- }
1907
- if (!(clientSecret && stripePromise)) return <LinearProgress />
1908
- if (isCheckout && stripePromise) return (
1909
- <EmbeddedCheckoutProvider stripe={stripePromise}
1910
- options={{
1911
- clientSecret,
1912
- onComplete: () => onChange(answertext || 'Completed checkout', field.id),
1913
- }}
1914
- >
1915
- <EmbeddedCheckout />
1916
- </EmbeddedCheckoutProvider>
1917
- )
1918
- return (
1919
- <Elements stripe={stripePromise} options={{
1920
- clientSecret,
1921
- }}>
1922
- <StripeForm businessName={businessName} onSuccess={() => onChange(answertext || 'Saved card details', field.id)}
1923
- cost={cost}
1924
- field={field}
1925
- />
1926
- </Elements>
1927
- )
1928
- }
1929
-
1930
- const StripeForm = ({ businessName, onSuccess, field, cost } : { businessName: string, onSuccess: () => void, field: FormField, cost: number }) => {
1931
- const stripe = useStripe();
1932
- const elements = useElements()
1933
-
1934
- const [ready, setReady] = useState(false)
1935
- const [errorMessage, setErrorMessage] = useState('');
1936
-
1937
- const handleSubmit = async (event: any) => {
1938
- // We don't want to let default form submission happen here,
1939
- // which would refresh the page.
1940
- event?.preventDefault();
1941
1667
 
1942
- if (!stripe || !elements) {
1943
- // Stripe.js hasn't yet loaded.
1944
- // Make sure to disable form submission until Stripe.js has loaded.
1945
- return null;
1946
- }
1947
-
1948
- const {error} = await (field.options?.chargeImmediately ? stripe.confirmPayment : stripe.confirmSetup)({
1949
- //`Elements` instance that was used to create the Payment Element
1950
- elements,
1951
- confirmParams: {
1952
- return_url: window.location.href,
1953
- },
1954
- redirect: 'if_required', // ensures the redirect url won't be used, unless the Bank redirect payment type is enabled (it's not, just card)
1955
- });
1956
-
1957
- if (error) {
1958
- // This point will only be reached if there is an immediate error when
1959
- // confirming the payment. Show error to your customer (for example, payment
1960
- // details incomplete)
1961
- setErrorMessage(error?.message ?? '');
1962
- } else {
1963
- onSuccess()
1964
- // Your customer will be redirected to your `return_url`. For some payment
1965
- // methods like iDEAL, your customer will be redirected to an intermediate
1966
- // site first to authorize the payment, then redirected to the `return_url`.
1967
- }
1968
- };
1969
-
1970
- return (
1971
- <form onSubmit={handleSubmit}>
1972
- <PaymentElement onReady={() => setReady(true)}
1973
- options={{
1974
- business: { name: businessName },
1975
- }}
1976
- />
1977
- <Button variant="contained" color="primary" type="submit" sx={{ mt: 1 }}
1978
- disabled={!(stripe && ready)}
1979
- >
1980
- {field.options?.chargeImmediately ? 'Make Payment' : 'Save Payment Details'}
1981
- </Button>
1982
-
1983
- {cost > 0 &&
1984
- <Typography sx={{ mt: 0.5 }}>
1985
- {
1986
- field.options?.customPriceMessage
1987
- ? field.options.customPriceMessage.replaceAll('{{PRICE}}', `$${(cost / 100).toFixed(2)}`)
1988
- : `You will be charged $${(cost / 100).toFixed(2)} ${field.options?.chargeImmediately ? '' : 'on form submission'}`
1989
- }
1990
- </Typography>
1991
- }
1992
-
1993
- {/* Show error message to your customers */}
1994
- {errorMessage &&
1995
- <Typography color="error" sx={{ mt: 0.5 }}>
1996
- {errorMessage}
1997
- </Typography>
1998
- }
1999
- </form>
2000
- )
2001
- }
1668
+ // StripeInput is shared between v1 and v2 forms
1669
+ // Both versions use the same implementation from inputs.tsx to ensure consistent behavior
1670
+ // and avoid code duplication. Re-exporting here maintains the pattern where forms.v2.tsx
1671
+ // only imports from inputs.v2.tsx
1672
+ export { StripeInput } from './inputs'
2002
1673
 
2003
1674
  export const Progress = ({ numerator, denominator, style, color } : { numerator: number, denominator: number, color?: string } & Styled) => (
2004
1675
  <Box sx={{ display: 'flex', alignItems: 'center', ...style }}>
@@ -2083,310 +1754,20 @@ export const DropdownInput = ({ field, value, onChange }: FormInputProps<'Dropdo
2083
1754
  )
2084
1755
  }
2085
1756
 
2086
- const choicesForDatabase: {
2087
- [index: string]: {
2088
- done: boolean,
2089
- records: DatabaseRecord[],
2090
- lastId?: string,
2091
- } | {
2092
- done: undefined,
2093
- records: undefined,
2094
- lastId?: string,
2095
- }
2096
- } = {}
2097
- const preventRefetch: Record<string, boolean> = {}
2098
-
2099
- const LOAD_CHOICES_LIMIT = 500
2100
- const useDatabaseChoices = ({ databaseId='', field, otherAnswers } : { databaseId?: string, field: FormField, otherAnswers?: DatabaseSelectResponse[] }) => {
2101
- const session = useResolvedSession()
2102
- const [renderCount, setRenderCount] = useState(0)
2103
-
2104
- // todo: make searchable, don't load all
2105
- useEffect(() => {
2106
- if (choicesForDatabase[databaseId]?.done) return
2107
- if (renderCount > 100) return // limit to 50000 entries / prevent infinite looping
2108
- const choices = choicesForDatabase[databaseId]?.records ?? []
2109
- const lastId = choicesForDatabase[databaseId]?.lastId
2110
-
2111
- if (preventRefetch[databaseId + field.id + lastId]) return
2112
- preventRefetch[databaseId + field.id + lastId] = true
2113
-
2114
- session.api.form_fields.load_choices_from_database({
2115
- fieldId: field.id,
2116
- lastId,
2117
- limit: LOAD_CHOICES_LIMIT,
2118
- databaseId, // overrides fieldId, supports using Database question in Table Input
2119
- })
2120
- .then(({ choices: newChoices }) => {
2121
- choicesForDatabase[databaseId] = {
2122
- lastId: newChoices?.[newChoices.length - 1]?.id,
2123
- records: [...choices, ...newChoices]
2124
- .sort((c1, c2) => (
2125
- label_for_database_record(field, c1)
2126
- .localeCompare(label_for_database_record(field, c2))
2127
- )
2128
- ),
2129
- done: newChoices.length < LOAD_CHOICES_LIMIT,
2130
- }
2131
- setRenderCount(r => r + 1)
2132
- })
2133
- .catch(err => {
2134
- console.error(err)
2135
- preventRefetch[databaseId + field.id + lastId] = false
2136
- })
2137
- }, [session, field, databaseId, renderCount])
2138
-
2139
- const addChoice = useCallback((record: DatabaseRecord) => {
2140
- if (!choicesForDatabase[databaseId]) {
2141
- choicesForDatabase[databaseId] = {
2142
- done: false,
2143
- records: [],
2144
- }
2145
- }
2146
- choicesForDatabase[databaseId].records!.push(record)
2147
- }, [choicesForDatabase, databaseId])
2148
-
2149
- return {
2150
- addChoice,
2151
- doneLoading: choicesForDatabase[databaseId]?.done ?? false,
2152
- choices: [
2153
- ...choicesForDatabase[databaseId]?.records ?? [],
2154
- ...(otherAnswers || []).map(v => ({
2155
- id: v.text,
2156
- databaseId,
2157
- values: [{ label: field.options?.databaseLabel || '', type: 'Text', value: v.text }],
2158
- }) as Pick<DatabaseRecord, 'id' | 'values' | 'databaseId'>)
2159
- ],
2160
- renderCount,
2161
- }
2162
- }
2163
-
2164
-
2165
- const label_for_database_record = (field: FormField, record?: Pick<DatabaseRecord, 'values'>) => {
2166
- if (!record) return ''
2167
-
2168
- const addedLabels = (
2169
- (field.options?.databaseLabels || [])
2170
- .map(l => record.values.find(v => v.label === l)?.value?.toString())
2171
- .filter(v => v?.trim())
2172
- ) as string[]
2173
-
2174
- return (
2175
- (record.values.find(v => v.label === field.options?.databaseLabel)?.value?.toString() ?? '')
2176
- + (
2177
- addedLabels.length
2178
- ? ` (${addedLabels.join(', ')})`
2179
- : ''
2180
- )
2181
- )
2182
- }
2183
-
2184
- const get_other_answers = (_value?: DatabaseSelectResponse[], typing?: string) => {
2185
- try {
2186
- const existing = (
2187
- (_value || [])
2188
- .filter(v => typeof v === 'string' || v.recordId === v.text)
2189
- .map(v => typeof v === 'string' ? { databaseId: '', recordId: v, text: v } : v)
2190
- )
2191
- if (typing) {
2192
- existing.push({ text: typing, databaseId: '', recordId: typing })
2193
- }
2194
-
2195
- return existing
2196
- } catch(err) { console.error(err) }
2197
-
2198
- return []
2199
- }
1757
+ // DatabaseSelectInput logic is shared with inputs.tsx to avoid duplication
1758
+ // Import the interface and component from the shared implementation
1759
+ import { AddToDatabaseProps as AddToDatabasePropsImported, DatabaseSelectInput as SharedDatabaseSelectInput } from './inputs'
2200
1760
 
2201
- export interface AddToDatabaseProps {
2202
- databaseId: string,
2203
- onAdd: (record: DatabaseRecord) => void
2204
- }
1761
+ // Re-export the interface for external use
1762
+ export type AddToDatabaseProps = AddToDatabasePropsImported
2205
1763
 
2206
- export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onChange, onDatabaseSelect, responses, size, disabled, enduser }: FormInputProps<'Database Select'> & {
1764
+ // Wrap the shared DatabaseSelectInput component with v2-specific props
1765
+ export const DatabaseSelectInput = (props: FormInputProps<'Database Select'> & {
2207
1766
  responses: FormResponseValue[],
2208
1767
  AddToDatabase?: React.JSXElementConstructor<AddToDatabaseProps>,
2209
1768
  }) => {
2210
- const [typing, setTyping] = useState('')
2211
- const { addChoice, choices, doneLoading } = useDatabaseChoices({
2212
- databaseId: field.options?.databaseId,
2213
- field,
2214
- otherAnswers: get_other_answers(_value, field?.options?.other ? typing : undefined),
2215
- })
2216
-
2217
- const value = React.useMemo(() => {
2218
- try {
2219
- // if the value is a string (some single answer that was save), make sure we coerce to array
2220
- const __value = typeof _value === 'string' ? [_value] : _value
2221
- return (
2222
- (__value?.map(v =>
2223
- choices.find(c =>
2224
- c.id === v.recordId || (typeof v === 'string' && label_for_database_record(field, c) === v)
2225
- )
2226
- )?.filter(v => v!) ?? []) as DatabaseRecord[]
2227
- )
2228
- } catch(err) {
2229
- console.error('Error resolving database answers for _value', err)
2230
- return []
2231
- }
2232
- }, [_value, choices, field])
2233
-
2234
- const filterResponse = useMemo(() => (
2235
- field.options?.databaseFilter?.fieldId
2236
- ? responses.find(r => r.fieldId === field.options?.databaseFilter?.fieldId)?.answer?.value
2237
- : undefined
2238
- ), [responses, field.options?.databaseFilter])
2239
-
2240
- // State filtering logic similar to Insurance component
2241
- const addressQuestion = useMemo(() => responses?.find(r => {
2242
- if (r.answer.type !== 'Address') return false
2243
- if (r.field.intakeField !== 'Address') return false
2244
-
2245
- // make sure state is actually defined (in case of multiple address questions, where 1+ are blank)
2246
- if (!r.answer.value?.state) return false
2247
-
2248
- return true
2249
- }), [responses])
2250
-
2251
- const state = useMemo(() => (
2252
- field.options?.filterByEnduserState
2253
- ? ((addressQuestion?.answer?.type === 'Address' ? addressQuestion?.answer?.value?.state : undefined) || enduser?.state)
2254
- : undefined
2255
- ), [enduser?.state, addressQuestion, field.options?.filterByEnduserState])
2256
-
2257
- const filteredChoicesWithPotentialDuplicates = useMemo(() => {
2258
- if (!choices) return []
2259
- if (!filterResponse) return choices
2260
- if (!field?.options?.databaseFilter?.databaseLabel)
2261
- if (!value || value.length === 0) return choices
2262
-
2263
- return (
2264
- choices
2265
- .filter(c => {
2266
- const v = c.values.find(_v => _v.label === field.options?.databaseFilter?.databaseLabel)?.value
2267
- if (!v) return true
2268
-
2269
- // use .text on r values to handle Database Select types as filter source (in addition to basic text and list of text)
2270
-
2271
- if (typeof v === 'object') {
2272
- return !!(
2273
- Object.values(v).find(oVal => (
2274
- typeof oVal === 'string' || typeof oVal === 'number'
2275
- ? (
2276
- Array.isArray(filterResponse)
2277
- ? (filterResponse as any[]).find(r => r === oVal.toString() || (typeof r === 'object' && r.text === oVal))
2278
- : (typeof filterResponse === 'string' || typeof filterResponse === 'number')
2279
- ? filterResponse.toString() === oVal.toString()
2280
- : false
2281
- )
2282
- : false
2283
- ))
2284
- )
2285
- }
2286
-
2287
- if (typeof v === 'string' || typeof v === 'number') {
2288
- return !!(
2289
- Array.isArray(filterResponse)
2290
- ? (filterResponse as any[]).find(r => r === v.toString() || (typeof r === 'object' && r.text === v))
2291
- : (typeof filterResponse === 'string' || typeof filterResponse === 'number')
2292
- ? filterResponse.toString() === v.toString()
2293
- : (typeof filterResponse === 'object' && (filterResponse as Address).city === v.toString()) ? true
2294
- : (typeof filterResponse === 'object' && (filterResponse as Address).state === v.toString()) ? true
2295
- : (typeof filterResponse === 'object' && (filterResponse as Address).zipCode === v.toString()) ? true
2296
- : false
2297
- )
2298
- }
2299
-
2300
- return false
2301
- })
2302
- )
2303
- }, [choices, filterResponse, field.options?.databaseFilter, value])
2304
-
2305
- // Apply state filtering as a secondary filter (doesn't modify existing logic)
2306
- const stateFilteredChoices = useMemo(() => {
2307
- if (!field.options?.filterByEnduserState || !state) {
2308
- return filteredChoicesWithPotentialDuplicates
2309
- }
2310
-
2311
- return filteredChoicesWithPotentialDuplicates.filter(c => {
2312
- const recordState = c.values.find(v => v.label?.trim()?.toLowerCase() === 'state')?.value?.toString() || ''
2313
- return !recordState || recordState === state
2314
- })
2315
- }, [filteredChoicesWithPotentialDuplicates, field.options?.filterByEnduserState, state])
2316
-
2317
- const filteredChoices = useMemo(() => {
2318
- const filtered = []
2319
-
2320
- const uniques = new Set<string>([])
2321
- for (const c of stateFilteredChoices) {
2322
- const text = label_for_database_record(field, c)
2323
- if (uniques.has(text)) continue // duplicate found
2324
-
2325
- uniques.add(text)
2326
- filtered.push(c)
2327
- }
2328
-
2329
- return filtered
2330
- }, [field, stateFilteredChoices])
2331
-
2332
- if (!doneLoading) return <LinearProgress />
2333
- return (
2334
- <>
2335
- <Autocomplete id={field.id} freeSolo={false} size={size}
2336
- componentsProps={{ popper: { sx: { wordBreak: "break-word" } } } }
2337
- options={filteredChoices} multiple={true}
2338
- getOptionLabel={o => (
2339
- Array.isArray(o) // edge case
2340
- ? ''
2341
- : label_for_database_record(field, o)
2342
- )}
2343
- value={value}
2344
- disabled={disabled}
2345
- onChange={(_, v) => {
2346
- if (v.length && onDatabaseSelect) {
2347
- onDatabaseSelect(
2348
- field.options?.radio
2349
- ? [v[v.length - 1]] // if radio, only last selected
2350
- : v
2351
- )
2352
- }
2353
- return onChange(
2354
- (
2355
- !field.options?.radio
2356
- ? v.map(_v => ({
2357
- databaseId: field.options?.databaseId!,
2358
- recordId: _v.id,
2359
- text: label_for_database_record(field, _v),
2360
- }))
2361
- : [{
2362
- databaseId: field.options?.databaseId!,
2363
- recordId: v[v.length -1]?.id ?? '',
2364
- text: label_for_database_record(field, v[v.length - 1]),
2365
- }]
2366
- ),
2367
- field.id,
2368
- )
2369
- }}
2370
- inputValue={typing}
2371
- onInputChange={(e, v) => e && setTyping(v)}
2372
- renderInput={params => <TextField {...params} InputProps={{ ...params.InputProps, sx: defaultInputProps.sx }} />}
2373
- // use custom Chip to ensure very long entries break properly (whitespace: normal)
2374
- renderTags={(value, getTagProps) =>
2375
- value.map((value, index) => (
2376
- <Chip
2377
- label={<Typography style={{whiteSpace: 'normal'}}>{Array.isArray(value) ? '' : label_for_database_record(field, value)}</Typography>}
2378
- {...getTagProps({ index })}
2379
- sx={{height:"100%", py: 0.5 }}
2380
- />
2381
- ))
2382
- }
2383
- />
2384
-
2385
- {AddToDatabase && field?.options?.allowAddToDatabase && (
2386
- <AddToDatabase databaseId={field.options?.databaseId!} onAdd={addChoice} />
2387
- )}
2388
- </>
2389
- )
1769
+ // Pass all props plus v2-specific defaultInputProps to the shared component
1770
+ return <SharedDatabaseSelectInput {...props} inputProps={defaultInputProps} />
2390
1771
  }
2391
1772
 
2392
1773
  type DisplayTermsResult = { displayTermsList: { term: string[] } }
@@ -3107,7 +2488,7 @@ export const contact_is_valid = (e: Partial<Enduser>) => {
3107
2488
  }
3108
2489
  }
3109
2490
 
3110
- export const RelatedContactsInput = ({ field, value: _value, onChange, ...props }: FormInputProps<'Related Contacts'>) => {
2491
+ export const RelatedContactsInput = ({ field, value: _value, onChange, error: parentError, ...props }: FormInputProps<'Related Contacts'>) => {
3111
2492
  // safeguard against any rogue values like empty string
3112
2493
  const value = Array.isArray(_value) ? _value : []
3113
2494
 
@@ -3182,7 +2563,7 @@ export const RelatedContactsInput = ({ field, value: _value, onChange, ...props
3182
2563
  <Grid item xs={4}>
3183
2564
  <TextField label="Phone Number" size="small" fullWidth
3184
2565
  InputProps={defaultInputProps}
3185
- value={phone} onChange={e => onChange(value.map((v, i) => i === editing ? { ...v, phone: e.target.value } : v), field.id)}
2566
+ value={phone} onChange={e => onChange(value.map((v, i) => i === editing ? { ...v, phone: e.target.value.trim() } : v), field.id)}
3186
2567
  />
3187
2568
  </Grid>
3188
2569
  }
@@ -3236,7 +2617,7 @@ export const RelatedContactsInput = ({ field, value: _value, onChange, ...props
3236
2617
  }
3237
2618
 
3238
2619
  <Grid item sx={{ my: 0.75 }}>
3239
- <Button variant="outlined" onClick={() => setEditing(-1)} size="small">
2620
+ <Button variant="outlined" onClick={() => setEditing(-1)} size="small" disabled={!!errorMessage || !!parentError}>
3240
2621
  Save Contact
3241
2622
  </Button>
3242
2623
  </Grid>