@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.
- package/lib/cjs/Forms/forms.js +1 -1
- package/lib/cjs/Forms/forms.js.map +1 -1
- package/lib/cjs/Forms/forms.v2.d.ts +1 -1
- package/lib/cjs/Forms/forms.v2.js +1 -1
- package/lib/cjs/Forms/forms.v2.js.map +1 -1
- package/lib/cjs/Forms/hooks.d.ts.map +1 -1
- package/lib/cjs/Forms/hooks.js +24 -0
- package/lib/cjs/Forms/hooks.js.map +1 -1
- package/lib/cjs/Forms/inputs.d.ts +6 -3
- package/lib/cjs/Forms/inputs.d.ts.map +1 -1
- package/lib/cjs/Forms/inputs.js +171 -44
- package/lib/cjs/Forms/inputs.js.map +1 -1
- package/lib/cjs/Forms/inputs.v2.d.ts +7 -11
- package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
- package/lib/cjs/Forms/inputs.v2.js +16 -445
- package/lib/cjs/Forms/inputs.v2.js.map +1 -1
- package/lib/esm/CMS/components.d.ts +0 -1
- package/lib/esm/CMS/components.d.ts.map +1 -1
- package/lib/esm/Forms/form_responses.d.ts +0 -1
- package/lib/esm/Forms/form_responses.d.ts.map +1 -1
- package/lib/esm/Forms/forms.d.ts +3 -3
- package/lib/esm/Forms/forms.js +1 -1
- package/lib/esm/Forms/forms.js.map +1 -1
- package/lib/esm/Forms/forms.v2.d.ts +4 -4
- package/lib/esm/Forms/forms.v2.js +1 -1
- package/lib/esm/Forms/forms.v2.js.map +1 -1
- package/lib/esm/Forms/hooks.d.ts +0 -1
- package/lib/esm/Forms/hooks.d.ts.map +1 -1
- package/lib/esm/Forms/hooks.js +24 -0
- package/lib/esm/Forms/hooks.js.map +1 -1
- package/lib/esm/Forms/inputs.d.ts +7 -4
- package/lib/esm/Forms/inputs.d.ts.map +1 -1
- package/lib/esm/Forms/inputs.js +173 -46
- package/lib/esm/Forms/inputs.js.map +1 -1
- package/lib/esm/Forms/inputs.v2.d.ts +8 -12
- package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
- package/lib/esm/Forms/inputs.v2.js +17 -446
- package/lib/esm/Forms/inputs.v2.js.map +1 -1
- package/lib/esm/controls.d.ts +2 -2
- package/lib/esm/inputs.d.ts +1 -1
- package/lib/esm/inputs.native.d.ts +0 -1
- package/lib/esm/inputs.native.d.ts.map +1 -1
- package/lib/esm/state.d.ts +315 -315
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/Forms/forms.tsx +1 -1
- package/src/Forms/forms.v2.tsx +1 -1
- package/src/Forms/hooks.tsx +33 -5
- package/src/Forms/inputs.tsx +224 -35
- package/src/Forms/inputs.v2.tsx +20 -639
package/src/Forms/inputs.v2.tsx
CHANGED
|
@@ -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,
|
|
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,
|
|
8
|
-
import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions,
|
|
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
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
|
2202
|
-
|
|
2203
|
-
onAdd: (record: DatabaseRecord) => void
|
|
2204
|
-
}
|
|
1761
|
+
// Re-export the interface for external use
|
|
1762
|
+
export type AddToDatabaseProps = AddToDatabasePropsImported
|
|
2205
1763
|
|
|
2206
|
-
|
|
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
|
-
|
|
2211
|
-
|
|
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>
|