refactor: ♻️ Rename step to block
This commit is contained in:
@ -0,0 +1,55 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { ChoiceInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type ChoiceInputSettingsBodyProps = {
|
||||
options?: ChoiceInputOptions
|
||||
onOptionsChange: (options: ChoiceInputOptions) => void
|
||||
}
|
||||
|
||||
export const ChoiceInputSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: ChoiceInputSettingsBodyProps) => {
|
||||
const handleIsMultipleChange = (isMultipleChoice: boolean) =>
|
||||
options && onOptionsChange({ ...options, isMultipleChoice })
|
||||
const handleButtonLabelChange = (buttonLabel: string) =>
|
||||
options && onOptionsChange({ ...options, buttonLabel })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
options && onOptionsChange({ ...options, variableId: variable?.id })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<SwitchWithLabel
|
||||
id={'is-multiple'}
|
||||
label={'Multiple choice?'}
|
||||
initialValue={options?.isMultipleChoice ?? false}
|
||||
onCheckChange={handleIsMultipleChange}
|
||||
/>
|
||||
{options?.isMultipleChoice && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options?.buttonLabel ?? 'Send'}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options?.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { FormLabel, Stack, Text } from '@chakra-ui/react'
|
||||
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { CodeOptions } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
options: CodeOptions
|
||||
onOptionsChange: (options: CodeOptions) => void
|
||||
}
|
||||
|
||||
export const CodeSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const handleNameChange = (name: string) =>
|
||||
onOptionsChange({ ...options, name })
|
||||
const handleCodeChange = (content: string) =>
|
||||
onOptionsChange({ ...options, content })
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="name">
|
||||
Name:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="name"
|
||||
defaultValue={options.name}
|
||||
onChange={handleNameChange}
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Code:</Text>
|
||||
<CodeEditor
|
||||
value={options.content ?? ''}
|
||||
lang="js"
|
||||
onChange={handleCodeChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { Input } from 'components/shared/Textbox/Input'
|
||||
import { TableListItemProps } from 'components/shared/TableList'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { Comparison, Variable, ComparisonOperators } from 'models'
|
||||
|
||||
export const ComparisonItem = ({
|
||||
item,
|
||||
onItemChange,
|
||||
}: TableListItemProps<Comparison>) => {
|
||||
const handleSelectVariable = (variable?: Variable) => {
|
||||
if (variable?.id === item.variableId) return
|
||||
onItemChange({ ...item, variableId: variable?.id })
|
||||
}
|
||||
|
||||
const handleSelectComparisonOperator = (
|
||||
comparisonOperator: ComparisonOperators
|
||||
) => {
|
||||
if (comparisonOperator === item.comparisonOperator) return
|
||||
onItemChange({ ...item, comparisonOperator })
|
||||
}
|
||||
const handleChangeValue = (value: string) => {
|
||||
if (value === item.value) return
|
||||
onItemChange({ ...item, value })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<VariableSearchInput
|
||||
initialVariableId={item.variableId}
|
||||
onSelectVariable={handleSelectVariable}
|
||||
placeholder="Search for a variable"
|
||||
/>
|
||||
<DropdownList<ComparisonOperators>
|
||||
currentItem={item.comparisonOperator}
|
||||
onItemSelect={handleSelectComparisonOperator}
|
||||
items={Object.values(ComparisonOperators)}
|
||||
placeholder="Select an operator"
|
||||
/>
|
||||
{item.comparisonOperator !== ComparisonOperators.IS_SET && (
|
||||
<Input
|
||||
defaultValue={item.value ?? ''}
|
||||
onChange={handleChangeValue}
|
||||
placeholder="Type a value..."
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { TableList } from 'components/shared/TableList'
|
||||
import {
|
||||
Comparison,
|
||||
ConditionItem,
|
||||
ConditionBlock,
|
||||
LogicalOperator,
|
||||
} from 'models'
|
||||
import React from 'react'
|
||||
import { ComparisonItem } from './ComparisonsItem'
|
||||
|
||||
type ConditionSettingsBodyProps = {
|
||||
block: ConditionBlock
|
||||
onItemChange: (updates: Partial<ConditionItem>) => void
|
||||
}
|
||||
|
||||
export const ConditionSettingsBody = ({
|
||||
block,
|
||||
onItemChange,
|
||||
}: ConditionSettingsBodyProps) => {
|
||||
const itemContent = block.items[0].content
|
||||
|
||||
const handleComparisonsChange = (comparisons: Comparison[]) =>
|
||||
onItemChange({ content: { ...itemContent, comparisons } })
|
||||
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>
|
||||
onItemChange({ content: { ...itemContent, logicalOperator } })
|
||||
|
||||
return (
|
||||
<TableList<Comparison>
|
||||
initialItems={itemContent.comparisons}
|
||||
onItemsChange={handleComparisonsChange}
|
||||
Item={ComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList<LogicalOperator>
|
||||
currentItem={itemContent.logicalOperator}
|
||||
onItemSelect={handleLogicalOperatorChange}
|
||||
items={Object.values(LogicalOperator)}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
addLabel="Add a comparison"
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { ConditionSettingsBody } from './ConditonSettingsBody'
|
@ -0,0 +1,89 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { DateInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type DateInputSettingsBodyProps = {
|
||||
options: DateInputOptions
|
||||
onOptionsChange: (options: DateInputOptions) => void
|
||||
}
|
||||
|
||||
export const DateInputSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: DateInputSettingsBodyProps) => {
|
||||
const handleFromChange = (from: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options?.labels, from } })
|
||||
const handleToChange = (to: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options?.labels, to } })
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
|
||||
const handleIsRangeChange = (isRange: boolean) =>
|
||||
onOptionsChange({ ...options, isRange })
|
||||
const handleHasTimeChange = (hasTime: boolean) =>
|
||||
onOptionsChange({ ...options, hasTime })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<SwitchWithLabel
|
||||
id="is-range"
|
||||
label={'Is range?'}
|
||||
initialValue={options.isRange}
|
||||
onCheckChange={handleIsRangeChange}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
id="with-time"
|
||||
label={'With time?'}
|
||||
initialValue={options.isRange}
|
||||
onCheckChange={handleHasTimeChange}
|
||||
/>
|
||||
{options.isRange && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="from">
|
||||
From label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="from"
|
||||
defaultValue={options.labels.from}
|
||||
onChange={handleFromChange}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{options?.isRange && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="to">
|
||||
To label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="to"
|
||||
defaultValue={options.labels.to}
|
||||
onChange={handleToChange}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.button}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { EmailInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type EmailInputSettingsBodyProps = {
|
||||
options: EmailInputOptions
|
||||
onOptionsChange: (options: EmailInputOptions) => void
|
||||
}
|
||||
|
||||
export const EmailInputSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: EmailInputSettingsBodyProps) => {
|
||||
const handlePlaceholderChange = (placeholder: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
const handleRetryMessageChange = (retryMessageContent: string) =>
|
||||
onOptionsChange({ ...options, retryMessageContent })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="placeholder">
|
||||
Placeholder:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="placeholder"
|
||||
defaultValue={options.labels.placeholder}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.button}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="retry">
|
||||
Retry message:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="retry"
|
||||
defaultValue={options.retryMessageContent}
|
||||
onChange={handleRetryMessageChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
FormLabel,
|
||||
Stack,
|
||||
Tag,
|
||||
} from '@chakra-ui/react'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { GoogleAnalyticsOptions } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
options?: GoogleAnalyticsOptions
|
||||
onOptionsChange: (options: GoogleAnalyticsOptions) => void
|
||||
}
|
||||
|
||||
export const GoogleAnalyticsSettings = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const handleTrackingIdChange = (trackingId: string) =>
|
||||
onOptionsChange({ ...options, trackingId })
|
||||
|
||||
const handleCategoryChange = (category: string) =>
|
||||
onOptionsChange({ ...options, category })
|
||||
|
||||
const handleActionChange = (action: string) =>
|
||||
onOptionsChange({ ...options, action })
|
||||
|
||||
const handleLabelChange = (label: string) =>
|
||||
onOptionsChange({ ...options, label })
|
||||
|
||||
const handleValueChange = (value?: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
value: value ? parseFloat(value) : undefined,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="tracking-id">
|
||||
Tracking ID:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="tracking-id"
|
||||
defaultValue={options?.trackingId ?? ''}
|
||||
placeholder="G-123456..."
|
||||
onChange={handleTrackingIdChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="category">
|
||||
Event category:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="category"
|
||||
defaultValue={options?.category ?? ''}
|
||||
placeholder="Example: Typebot"
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="action">
|
||||
Event action:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="action"
|
||||
defaultValue={options?.action ?? ''}
|
||||
placeholder="Example: Submit email"
|
||||
onChange={handleActionChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Advanced
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="label">
|
||||
Event label <Tag>Optional</Tag>:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="label"
|
||||
defaultValue={options?.label ?? ''}
|
||||
placeholder="Example: Campaign Z"
|
||||
onChange={handleLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="value">
|
||||
Event value <Tag>Optional</Tag>:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="value"
|
||||
defaultValue={options?.value?.toString() ?? ''}
|
||||
placeholder="Example: 0"
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
</Stack>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { Input } from 'components/shared/Textbox/Input'
|
||||
import { TableListItemProps } from 'components/shared/TableList'
|
||||
import { Cell } from 'models'
|
||||
|
||||
export const CellWithValueStack = ({
|
||||
item,
|
||||
onItemChange,
|
||||
columns,
|
||||
}: TableListItemProps<Cell> & { columns: string[] }) => {
|
||||
const handleColumnSelect = (column: string) => {
|
||||
if (item.column === column) return
|
||||
onItemChange({ ...item, column })
|
||||
}
|
||||
const handleValueChange = (value: string) => {
|
||||
if (item.value === value) return
|
||||
onItemChange({ ...item, value })
|
||||
}
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px" w="full">
|
||||
<DropdownList<string>
|
||||
currentItem={item.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
placeholder="Select a column"
|
||||
/>
|
||||
<Input
|
||||
defaultValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
placeholder="Type a value..."
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { TableListItemProps } from 'components/shared/TableList'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { ExtractingCell, Variable } from 'models'
|
||||
|
||||
export const CellWithVariableIdStack = ({
|
||||
item,
|
||||
onItemChange,
|
||||
columns,
|
||||
}: TableListItemProps<ExtractingCell> & { columns: string[] }) => {
|
||||
const handleColumnSelect = (column: string) => {
|
||||
if (item.column === column) return
|
||||
onItemChange({ ...item, column })
|
||||
}
|
||||
|
||||
const handleVariableIdChange = (variable?: Variable) => {
|
||||
if (item.variableId === variable?.id) return
|
||||
onItemChange({ ...item, variableId: variable?.id })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList<string>
|
||||
currentItem={item.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
placeholder="Select a column"
|
||||
/>
|
||||
<VariableSearchInput
|
||||
initialVariableId={item.variableId}
|
||||
onSelectVariable={handleVariableIdChange}
|
||||
placeholder="Select a variable"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
Text,
|
||||
Image,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { GoogleLogo } from 'assets/logos'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import { Info } from 'components/shared/Info'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import React from 'react'
|
||||
import { getGoogleSheetsConsentScreenUrl } from 'services/integrations'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
blockId: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const GoogleSheetConnectModal = ({
|
||||
blockId,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Connect Spreadsheets</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing="6">
|
||||
<Info>
|
||||
Typebot needs access to Google Drive in order to list all your
|
||||
spreadsheets. It also needs access to your spreadsheets in order to
|
||||
fetch or inject data in it.
|
||||
</Info>
|
||||
<Text>
|
||||
Make sure to check all the permissions so that the integration works
|
||||
as expected:
|
||||
</Text>
|
||||
<Image
|
||||
src="/images/google-spreadsheets-scopes.jpeg"
|
||||
alt="Google Spreadsheets checkboxes"
|
||||
/>
|
||||
<Flex>
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
leftIcon={<GoogleLogo />}
|
||||
data-testid="google"
|
||||
isLoading={['loading', 'authenticated'].includes(status)}
|
||||
variant="outline"
|
||||
href={getGoogleSheetsConsentScreenUrl(
|
||||
window.location.href,
|
||||
blockId,
|
||||
workspace?.id
|
||||
)}
|
||||
mx="auto"
|
||||
>
|
||||
Continue with Google
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
import { Divider, Stack, Text, useDisclosure } from '@chakra-ui/react'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
Cell,
|
||||
CredentialsType,
|
||||
ExtractingCell,
|
||||
GoogleSheetsAction,
|
||||
GoogleSheetsGetOptions,
|
||||
GoogleSheetsInsertRowOptions,
|
||||
GoogleSheetsOptions,
|
||||
GoogleSheetsUpdateRowOptions,
|
||||
} from 'models'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Sheet, useSheets } from 'services/integrations'
|
||||
import { isDefined, omit } from 'utils'
|
||||
import { SheetsDropdown } from './SheetsDropdown'
|
||||
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
|
||||
import { CellWithValueStack } from './CellWithValueStack'
|
||||
import { CellWithVariableIdStack } from './CellWithVariableIdStack'
|
||||
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||
import { GoogleSheetConnectModal } from './GoogleSheetsConnectModal'
|
||||
|
||||
type Props = {
|
||||
options: GoogleSheetsOptions
|
||||
onOptionsChange: (options: GoogleSheetsOptions) => void
|
||||
blockId: string
|
||||
}
|
||||
|
||||
export const GoogleSheetsSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
blockId,
|
||||
}: Props) => {
|
||||
const { save } = useTypebot()
|
||||
const { sheets, isLoading } = useSheets({
|
||||
credentialsId: options?.credentialsId,
|
||||
spreadsheetId: options?.spreadsheetId,
|
||||
})
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const sheet = useMemo(
|
||||
() => sheets?.find((s) => s.id === options?.sheetId),
|
||||
[sheets, options?.sheetId]
|
||||
)
|
||||
const handleCredentialsIdChange = (credentialsId?: string) =>
|
||||
onOptionsChange({ ...omit(options, 'credentialsId'), credentialsId })
|
||||
const handleSpreadsheetIdChange = (spreadsheetId: string) =>
|
||||
onOptionsChange({ ...options, spreadsheetId })
|
||||
const handleSheetIdChange = (sheetId: string) =>
|
||||
onOptionsChange({ ...options, sheetId })
|
||||
|
||||
const handleActionChange = (action: GoogleSheetsAction) => {
|
||||
switch (action) {
|
||||
case GoogleSheetsAction.GET: {
|
||||
const newOptions: GoogleSheetsGetOptions = {
|
||||
...options,
|
||||
action,
|
||||
cellsToExtract: [],
|
||||
}
|
||||
return onOptionsChange({ ...newOptions })
|
||||
}
|
||||
case GoogleSheetsAction.INSERT_ROW: {
|
||||
const newOptions: GoogleSheetsInsertRowOptions = {
|
||||
...options,
|
||||
action,
|
||||
cellsToInsert: [],
|
||||
}
|
||||
return onOptionsChange({ ...newOptions })
|
||||
}
|
||||
case GoogleSheetsAction.UPDATE_ROW: {
|
||||
const newOptions: GoogleSheetsUpdateRowOptions = {
|
||||
...options,
|
||||
action,
|
||||
cellsToUpsert: [],
|
||||
}
|
||||
return onOptionsChange({ ...newOptions })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateNewClick = async () => {
|
||||
await save()
|
||||
onOpen()
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CredentialsDropdown
|
||||
type={CredentialsType.GOOGLE_SHEETS}
|
||||
currentCredentialsId={options?.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsIdChange}
|
||||
onCreateNewClick={handleCreateNewClick}
|
||||
/>
|
||||
<GoogleSheetConnectModal
|
||||
blockId={blockId}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{options?.credentialsId && (
|
||||
<SpreadsheetsDropdown
|
||||
credentialsId={options.credentialsId}
|
||||
spreadsheetId={options.spreadsheetId}
|
||||
onSelectSpreadsheetId={handleSpreadsheetIdChange}
|
||||
/>
|
||||
)}
|
||||
{options?.spreadsheetId && options.credentialsId && (
|
||||
<SheetsDropdown
|
||||
sheets={sheets ?? []}
|
||||
isLoading={isLoading}
|
||||
sheetId={options.sheetId}
|
||||
onSelectSheetId={handleSheetIdChange}
|
||||
/>
|
||||
)}
|
||||
{options?.spreadsheetId &&
|
||||
options.credentialsId &&
|
||||
isDefined(options.sheetId) && (
|
||||
<>
|
||||
<Divider />
|
||||
<DropdownList<GoogleSheetsAction>
|
||||
currentItem={'action' in options ? options.action : undefined}
|
||||
onItemSelect={handleActionChange}
|
||||
items={Object.values(GoogleSheetsAction)}
|
||||
placeholder="Select an operation"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{'action' in options && (
|
||||
<ActionOptions
|
||||
options={options}
|
||||
sheet={sheet}
|
||||
onOptionsChange={onOptionsChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const ActionOptions = ({
|
||||
options,
|
||||
sheet,
|
||||
onOptionsChange,
|
||||
}: {
|
||||
options:
|
||||
| GoogleSheetsGetOptions
|
||||
| GoogleSheetsInsertRowOptions
|
||||
| GoogleSheetsUpdateRowOptions
|
||||
sheet?: Sheet
|
||||
onOptionsChange: (options: GoogleSheetsOptions) => void
|
||||
}) => {
|
||||
const handleInsertColumnsChange = (cellsToInsert: Cell[]) =>
|
||||
onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions)
|
||||
|
||||
const handleUpsertColumnsChange = (cellsToUpsert: Cell[]) =>
|
||||
onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions)
|
||||
|
||||
const handleReferenceCellChange = (referenceCell: Cell) =>
|
||||
onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions)
|
||||
|
||||
const handleExtractingCellsChange = (cellsToExtract: ExtractingCell[]) =>
|
||||
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
|
||||
|
||||
const UpdatingCellItem = useMemo(
|
||||
() => (props: TableListItemProps<Cell>) =>
|
||||
<CellWithValueStack {...props} columns={sheet?.columns ?? []} />,
|
||||
[sheet?.columns]
|
||||
)
|
||||
|
||||
const ExtractingCellItem = useMemo(
|
||||
() => (props: TableListItemProps<ExtractingCell>) =>
|
||||
<CellWithVariableIdStack {...props} columns={sheet?.columns ?? []} />,
|
||||
[sheet?.columns]
|
||||
)
|
||||
|
||||
switch (options.action) {
|
||||
case GoogleSheetsAction.INSERT_ROW:
|
||||
return (
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToInsert}
|
||||
onItemsChange={handleInsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
)
|
||||
case GoogleSheetsAction.UPDATE_ROW:
|
||||
return (
|
||||
<Stack>
|
||||
<Text>Row to select</Text>
|
||||
<CellWithValueStack
|
||||
columns={sheet?.columns ?? []}
|
||||
item={options.referenceCell ?? { id: 'reference' }}
|
||||
onItemChange={handleReferenceCellChange}
|
||||
/>
|
||||
<Text>Cells to update</Text>
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToUpsert}
|
||||
onItemsChange={handleUpsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
case GoogleSheetsAction.GET:
|
||||
return (
|
||||
<Stack>
|
||||
<Text>Row to select</Text>
|
||||
<CellWithValueStack
|
||||
columns={sheet?.columns ?? []}
|
||||
item={options.referenceCell ?? { id: 'reference' }}
|
||||
onItemChange={handleReferenceCellChange}
|
||||
/>
|
||||
<Text>Cells to extract</Text>
|
||||
<TableList<ExtractingCell>
|
||||
initialItems={options.cellsToExtract}
|
||||
onItemsChange={handleExtractingCellsChange}
|
||||
Item={ExtractingCellItem}
|
||||
addLabel="Add a value"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { Input } from '@chakra-ui/react'
|
||||
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
|
||||
import { useMemo } from 'react'
|
||||
import { Sheet } from 'services/integrations'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
sheets: Sheet[]
|
||||
isLoading: boolean
|
||||
sheetId?: string
|
||||
onSelectSheetId: (id: string) => void
|
||||
}
|
||||
|
||||
export const SheetsDropdown = ({
|
||||
sheets,
|
||||
isLoading,
|
||||
sheetId,
|
||||
onSelectSheetId,
|
||||
}: Props) => {
|
||||
const currentSheet = useMemo(
|
||||
() => sheets?.find((s) => s.id === sheetId),
|
||||
[sheetId, sheets]
|
||||
)
|
||||
|
||||
const handleSpreadsheetSelect = (name: string) => {
|
||||
const id = sheets?.find((s) => s.name === name)?.id
|
||||
if (isDefined(id)) onSelectSheetId(id)
|
||||
}
|
||||
|
||||
if (isLoading) return <Input value="Loading..." isDisabled />
|
||||
if (!sheets) return <Input value="No sheets found" isDisabled />
|
||||
return (
|
||||
<SearchableDropdown
|
||||
selectedItem={currentSheet?.name}
|
||||
items={(sheets ?? []).map((s) => s.name)}
|
||||
onValueChange={handleSpreadsheetSelect}
|
||||
placeholder={'Select the sheet'}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { Input, Tooltip } from '@chakra-ui/react'
|
||||
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
|
||||
import { useMemo } from 'react'
|
||||
import { useSpreadsheets } from 'services/integrations'
|
||||
|
||||
type Props = {
|
||||
credentialsId: string
|
||||
spreadsheetId?: string
|
||||
onSelectSpreadsheetId: (id: string) => void
|
||||
}
|
||||
|
||||
export const SpreadsheetsDropdown = ({
|
||||
credentialsId,
|
||||
spreadsheetId,
|
||||
onSelectSpreadsheetId,
|
||||
}: Props) => {
|
||||
const { spreadsheets, isLoading } = useSpreadsheets({
|
||||
credentialsId,
|
||||
})
|
||||
const currentSpreadsheet = useMemo(
|
||||
() => spreadsheets?.find((s) => s.id === spreadsheetId),
|
||||
[spreadsheetId, spreadsheets]
|
||||
)
|
||||
|
||||
const handleSpreadsheetSelect = (name: string) => {
|
||||
const id = spreadsheets?.find((s) => s.name === name)?.id
|
||||
if (id) onSelectSpreadsheetId(id)
|
||||
}
|
||||
if (isLoading) return <Input value="Loading..." isDisabled />
|
||||
if (!spreadsheets || spreadsheets.length === 0)
|
||||
return (
|
||||
<Tooltip label="No spreadsheets found, make sure you have at least one spreadsheet that contains a header row">
|
||||
<span>
|
||||
<Input value="No spreadsheets found" isDisabled />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
return (
|
||||
<SearchableDropdown
|
||||
selectedItem={currentSpreadsheet?.name}
|
||||
items={(spreadsheets ?? []).map((s) => s.name)}
|
||||
onValueChange={handleSpreadsheetSelect}
|
||||
placeholder={'Search for spreadsheet'}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { GoogleSheetsSettingsBody } from './GoogleSheetsSettingsBody'
|
@ -0,0 +1,95 @@
|
||||
import { FormLabel, HStack, Stack } from '@chakra-ui/react'
|
||||
import { SmartNumberInput } from 'components/shared/SmartNumberInput'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { NumberInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
import { removeUndefinedFields } from 'services/utils'
|
||||
|
||||
type NumberInputSettingsBodyProps = {
|
||||
options: NumberInputOptions
|
||||
onOptionsChange: (options: NumberInputOptions) => void
|
||||
}
|
||||
|
||||
export const NumberInputSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: NumberInputSettingsBodyProps) => {
|
||||
const handlePlaceholderChange = (placeholder: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||
const handleMinChange = (min?: number) =>
|
||||
onOptionsChange(removeUndefinedFields({ ...options, min }))
|
||||
const handleMaxChange = (max?: number) =>
|
||||
onOptionsChange(removeUndefinedFields({ ...options, max }))
|
||||
const handleBlockChange = (block?: number) =>
|
||||
onOptionsChange(removeUndefinedFields({ ...options, block }))
|
||||
const handleVariableChange = (variable?: Variable) => {
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="placeholder">
|
||||
Placeholder:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="placeholder"
|
||||
defaultValue={options.labels.placeholder}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options?.labels?.button ?? 'Send'}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<HStack justifyContent="space-between">
|
||||
<FormLabel mb="0" htmlFor="min">
|
||||
Min:
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="min"
|
||||
value={options.min}
|
||||
onValueChange={handleMinChange}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack justifyContent="space-between">
|
||||
<FormLabel mb="0" htmlFor="max">
|
||||
Max:
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="max"
|
||||
value={options.max}
|
||||
onValueChange={handleMaxChange}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack justifyContent="space-between">
|
||||
<FormLabel mb="0" htmlFor="step">
|
||||
Step:
|
||||
</FormLabel>
|
||||
<SmartNumberInput
|
||||
id="step"
|
||||
value={options.step}
|
||||
onValueChange={handleBlockChange}
|
||||
/>
|
||||
</HStack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
import {
|
||||
Stack,
|
||||
useDisclosure,
|
||||
Text,
|
||||
Select,
|
||||
HStack,
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
} from '@chakra-ui/react'
|
||||
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { CredentialsType, PaymentInputOptions, PaymentProvider } from 'models'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { currencies } from './currencies'
|
||||
import { StripeConfigModal } from './StripeConfigModal'
|
||||
|
||||
type Props = {
|
||||
options: PaymentInputOptions
|
||||
onOptionsChange: (options: PaymentInputOptions) => void
|
||||
}
|
||||
|
||||
export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [refreshCredentialsKey, setRefreshCredentialsKey] = useState(0)
|
||||
|
||||
const handleProviderChange = (provider: PaymentProvider) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
provider,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCredentialsSelect = (credentialsId?: string) => {
|
||||
setRefreshCredentialsKey(refreshCredentialsKey + 1)
|
||||
onOptionsChange({
|
||||
...options,
|
||||
credentialsId,
|
||||
})
|
||||
}
|
||||
|
||||
const handleAmountChange = (amount?: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
amount,
|
||||
})
|
||||
|
||||
const handleCurrencyChange = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
currency: e.target.value,
|
||||
})
|
||||
|
||||
const handleNameChange = (name: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
additionalInformation: { ...options.additionalInformation, name },
|
||||
})
|
||||
|
||||
const handleEmailChange = (email: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
additionalInformation: { ...options.additionalInformation, email },
|
||||
})
|
||||
|
||||
const handlePhoneNumberChange = (phoneNumber: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
additionalInformation: { ...options.additionalInformation, phoneNumber },
|
||||
})
|
||||
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
labels: { ...options.labels, button },
|
||||
})
|
||||
|
||||
const handleSuccessLabelChange = (success: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
labels: { ...options.labels, success },
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<Text>Provider:</Text>
|
||||
<DropdownList
|
||||
onItemSelect={handleProviderChange}
|
||||
items={Object.values(PaymentProvider)}
|
||||
currentItem={options.provider}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Account:</Text>
|
||||
<CredentialsDropdown
|
||||
type={CredentialsType.STRIPE}
|
||||
currentCredentialsId={options.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsSelect}
|
||||
onCreateNewClick={onOpen}
|
||||
refreshDropdownKey={refreshCredentialsKey}
|
||||
/>
|
||||
</Stack>
|
||||
<HStack>
|
||||
<Stack>
|
||||
<Text>Price amount:</Text>
|
||||
<Input
|
||||
onChange={handleAmountChange}
|
||||
defaultValue={options.amount}
|
||||
placeholder="30.00"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Currency:</Text>
|
||||
<Select
|
||||
placeholder="Select option"
|
||||
value={options.currency}
|
||||
onChange={handleCurrencyChange}
|
||||
>
|
||||
{currencies.map((currency) => (
|
||||
<option value={currency.code} key={currency.code}>
|
||||
{currency.code}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
</HStack>
|
||||
<Stack>
|
||||
<Text>Button label:</Text>
|
||||
<Input
|
||||
onChange={handleButtonLabelChange}
|
||||
defaultValue={options.labels.button}
|
||||
placeholder="Pay"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Success message:</Text>
|
||||
<Input
|
||||
onChange={handleSuccessLabelChange}
|
||||
defaultValue={options.labels.success ?? 'Success'}
|
||||
placeholder="Success"
|
||||
/>
|
||||
</Stack>
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Additional information
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<Stack>
|
||||
<Text>Name:</Text>
|
||||
<Input
|
||||
defaultValue={options.additionalInformation?.name ?? ''}
|
||||
onChange={handleNameChange}
|
||||
placeholder="John Smith"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Email:</Text>
|
||||
<Input
|
||||
defaultValue={options.additionalInformation?.email ?? ''}
|
||||
onChange={handleEmailChange}
|
||||
placeholder="john@gmail.com"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Phone number:</Text>
|
||||
<Input
|
||||
defaultValue={options.additionalInformation?.phoneNumber ?? ''}
|
||||
onChange={handlePhoneNumberChange}
|
||||
placeholder="+33XXXXXXXXX"
|
||||
/>
|
||||
</Stack>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<StripeConfigModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onNewCredentials={handleCredentialsSelect}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Stack,
|
||||
Text,
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { CredentialsType, StripeCredentialsData } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import { MoreInfoTooltip } from 'components/shared/MoreInfoTooltip'
|
||||
import { ExternalLinkIcon } from 'assets/icons'
|
||||
import { createCredentials } from 'services/credentials'
|
||||
import { omit } from 'utils'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onNewCredentials: (id: string) => void
|
||||
}
|
||||
|
||||
export const StripeConfigModal = ({
|
||||
isOpen,
|
||||
onNewCredentials,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const { showToast } = useToast()
|
||||
const [stripeConfig, setStripeConfig] = useState<
|
||||
StripeCredentialsData & { name: string }
|
||||
>({
|
||||
name: '',
|
||||
live: { publicKey: '', secretKey: '' },
|
||||
test: { publicKey: '', secretKey: '' },
|
||||
})
|
||||
|
||||
const handleNameChange = (name: string) =>
|
||||
setStripeConfig({
|
||||
...stripeConfig,
|
||||
name,
|
||||
})
|
||||
|
||||
const handlePublicKeyChange = (publicKey: string) =>
|
||||
setStripeConfig({
|
||||
...stripeConfig,
|
||||
live: { ...stripeConfig.live, publicKey },
|
||||
})
|
||||
|
||||
const handleSecretKeyChange = (secretKey: string) =>
|
||||
setStripeConfig({
|
||||
...stripeConfig,
|
||||
live: { ...stripeConfig.live, secretKey },
|
||||
})
|
||||
|
||||
const handleTestPublicKeyChange = (publicKey: string) =>
|
||||
setStripeConfig({
|
||||
...stripeConfig,
|
||||
test: { ...stripeConfig.test, publicKey },
|
||||
})
|
||||
|
||||
const handleTestSecretKeyChange = (secretKey: string) =>
|
||||
setStripeConfig({
|
||||
...stripeConfig,
|
||||
test: { ...stripeConfig.test, secretKey },
|
||||
})
|
||||
|
||||
const handleCreateClick = async () => {
|
||||
if (!user?.email || !workspace?.id) return
|
||||
setIsCreating(true)
|
||||
const { data, error } = await createCredentials({
|
||||
data: omit(stripeConfig, 'name'),
|
||||
name: stripeConfig.name,
|
||||
type: CredentialsType.STRIPE,
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
setIsCreating(false)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
if (!data?.credentials)
|
||||
return showToast({ description: "Credentials wasn't created" })
|
||||
onNewCredentials(data.credentials.id)
|
||||
onClose()
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Connect Stripe account</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Stack as="form" spacing={4}>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Account name:</FormLabel>
|
||||
<Input
|
||||
onChange={handleNameChange}
|
||||
placeholder="Typebot"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<Stack>
|
||||
<FormLabel>
|
||||
Test keys:{' '}
|
||||
<MoreInfoTooltip>
|
||||
Will be used when previewing the bot.
|
||||
</MoreInfoTooltip>
|
||||
</FormLabel>
|
||||
<HStack>
|
||||
<FormControl>
|
||||
<Input
|
||||
onChange={handleTestPublicKeyChange}
|
||||
placeholder="pk_test_..."
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Input
|
||||
onChange={handleTestSecretKeyChange}
|
||||
placeholder="sk_test_..."
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel>Live keys:</FormLabel>
|
||||
<HStack>
|
||||
<FormControl>
|
||||
<Input
|
||||
onChange={handlePublicKeyChange}
|
||||
placeholder="pk_live_..."
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Input
|
||||
onChange={handleSecretKeyChange}
|
||||
placeholder="sk_live_..."
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
</Stack>
|
||||
|
||||
<Text>
|
||||
(You can find your keys{' '}
|
||||
<NextChakraLink
|
||||
href="https://dashboard.stripe.com/apikeys"
|
||||
isExternal
|
||||
textDecor="underline"
|
||||
>
|
||||
here <ExternalLinkIcon />
|
||||
</NextChakraLink>
|
||||
)
|
||||
</Text>
|
||||
</Stack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={handleCreateClick}
|
||||
isDisabled={
|
||||
stripeConfig.live.publicKey === '' ||
|
||||
stripeConfig.name === '' ||
|
||||
stripeConfig.live.secretKey === ''
|
||||
}
|
||||
isLoading={isCreating}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -0,0 +1,545 @@
|
||||
// The STRIPE-supported currencies, sorted by code
|
||||
// https://gist.github.com/chrisdavies/9e3f00889fb764013339632bd3f2a71b
|
||||
|
||||
export const currencies = [
|
||||
{
|
||||
code: 'AED',
|
||||
description: 'United Arab Emirates Dirham',
|
||||
},
|
||||
{
|
||||
code: 'AFN',
|
||||
description: 'Afghan Afghani**',
|
||||
},
|
||||
{
|
||||
code: 'ALL',
|
||||
description: 'Albanian Lek',
|
||||
},
|
||||
{
|
||||
code: 'AMD',
|
||||
description: 'Armenian Dram',
|
||||
},
|
||||
{
|
||||
code: 'ANG',
|
||||
description: 'Netherlands Antillean Gulden',
|
||||
},
|
||||
{
|
||||
code: 'AOA',
|
||||
description: 'Angolan Kwanza**',
|
||||
},
|
||||
{
|
||||
code: 'ARS',
|
||||
description: 'Argentine Peso**',
|
||||
},
|
||||
{
|
||||
code: 'AUD',
|
||||
description: 'Australian Dollar',
|
||||
},
|
||||
{
|
||||
code: 'AWG',
|
||||
description: 'Aruban Florin',
|
||||
},
|
||||
{
|
||||
code: 'AZN',
|
||||
description: 'Azerbaijani Manat',
|
||||
},
|
||||
{
|
||||
code: 'BAM',
|
||||
description: 'Bosnia & Herzegovina Convertible Mark',
|
||||
},
|
||||
{
|
||||
code: 'BBD',
|
||||
description: 'Barbadian Dollar',
|
||||
},
|
||||
{
|
||||
code: 'BDT',
|
||||
description: 'Bangladeshi Taka',
|
||||
},
|
||||
{
|
||||
code: 'BGN',
|
||||
description: 'Bulgarian Lev',
|
||||
},
|
||||
{
|
||||
code: 'BIF',
|
||||
description: 'Burundian Franc',
|
||||
},
|
||||
{
|
||||
code: 'BMD',
|
||||
description: 'Bermudian Dollar',
|
||||
},
|
||||
{
|
||||
code: 'BND',
|
||||
description: 'Brunei Dollar',
|
||||
},
|
||||
{
|
||||
code: 'BOB',
|
||||
description: 'Bolivian Boliviano**',
|
||||
},
|
||||
{
|
||||
code: 'BRL',
|
||||
description: 'Brazilian Real**',
|
||||
},
|
||||
{
|
||||
code: 'BSD',
|
||||
description: 'Bahamian Dollar',
|
||||
},
|
||||
{
|
||||
code: 'BWP',
|
||||
description: 'Botswana Pula',
|
||||
},
|
||||
{
|
||||
code: 'BZD',
|
||||
description: 'Belize Dollar',
|
||||
},
|
||||
{
|
||||
code: 'CAD',
|
||||
description: 'Canadian Dollar',
|
||||
},
|
||||
{
|
||||
code: 'CDF',
|
||||
description: 'Congolese Franc',
|
||||
},
|
||||
{
|
||||
code: 'CHF',
|
||||
description: 'Swiss Franc',
|
||||
},
|
||||
{
|
||||
code: 'CLP',
|
||||
description: 'Chilean Peso**',
|
||||
},
|
||||
{
|
||||
code: 'CNY',
|
||||
description: 'Chinese Renminbi Yuan',
|
||||
},
|
||||
{
|
||||
code: 'COP',
|
||||
description: 'Colombian Peso**',
|
||||
},
|
||||
{
|
||||
code: 'CRC',
|
||||
description: 'Costa Rican Colón**',
|
||||
},
|
||||
{
|
||||
code: 'CVE',
|
||||
description: 'Cape Verdean Escudo**',
|
||||
},
|
||||
{
|
||||
code: 'CZK',
|
||||
description: 'Czech Koruna**',
|
||||
},
|
||||
{
|
||||
code: 'DJF',
|
||||
description: 'Djiboutian Franc**',
|
||||
},
|
||||
{
|
||||
code: 'DKK',
|
||||
description: 'Danish Krone',
|
||||
},
|
||||
{
|
||||
code: 'DOP',
|
||||
description: 'Dominican Peso',
|
||||
},
|
||||
{
|
||||
code: 'DZD',
|
||||
description: 'Algerian Dinar',
|
||||
},
|
||||
{
|
||||
code: 'EGP',
|
||||
description: 'Egyptian Pound',
|
||||
},
|
||||
{
|
||||
code: 'ETB',
|
||||
description: 'Ethiopian Birr',
|
||||
},
|
||||
{
|
||||
code: 'EUR',
|
||||
description: 'Euro',
|
||||
},
|
||||
{
|
||||
code: 'FJD',
|
||||
description: 'Fijian Dollar',
|
||||
},
|
||||
{
|
||||
code: 'FKP',
|
||||
description: 'Falkland Islands Pound**',
|
||||
},
|
||||
{
|
||||
code: 'GBP',
|
||||
description: 'British Pound',
|
||||
},
|
||||
{
|
||||
code: 'GEL',
|
||||
description: 'Georgian Lari',
|
||||
},
|
||||
{
|
||||
code: 'GIP',
|
||||
description: 'Gibraltar Pound',
|
||||
},
|
||||
{
|
||||
code: 'GMD',
|
||||
description: 'Gambian Dalasi',
|
||||
},
|
||||
{
|
||||
code: 'GNF',
|
||||
description: 'Guinean Franc**',
|
||||
},
|
||||
{
|
||||
code: 'GTQ',
|
||||
description: 'Guatemalan Quetzal**',
|
||||
},
|
||||
{
|
||||
code: 'GYD',
|
||||
description: 'Guyanese Dollar',
|
||||
},
|
||||
{
|
||||
code: 'HKD',
|
||||
description: 'Hong Kong Dollar',
|
||||
},
|
||||
{
|
||||
code: 'HNL',
|
||||
description: 'Honduran Lempira**',
|
||||
},
|
||||
{
|
||||
code: 'HRK',
|
||||
description: 'Croatian Kuna',
|
||||
},
|
||||
{
|
||||
code: 'HTG',
|
||||
description: 'Haitian Gourde',
|
||||
},
|
||||
{
|
||||
code: 'HUF',
|
||||
description: 'Hungarian Forint**',
|
||||
},
|
||||
{
|
||||
code: 'IDR',
|
||||
description: 'Indonesian Rupiah',
|
||||
},
|
||||
{
|
||||
code: 'ILS',
|
||||
description: 'Israeli New Sheqel',
|
||||
},
|
||||
{
|
||||
code: 'INR',
|
||||
description: 'Indian Rupee**',
|
||||
},
|
||||
{
|
||||
code: 'ISK',
|
||||
description: 'Icelandic Króna',
|
||||
},
|
||||
{
|
||||
code: 'JMD',
|
||||
description: 'Jamaican Dollar',
|
||||
},
|
||||
{
|
||||
code: 'JPY',
|
||||
description: 'Japanese Yen',
|
||||
},
|
||||
{
|
||||
code: 'KES',
|
||||
description: 'Kenyan Shilling',
|
||||
},
|
||||
{
|
||||
code: 'KGS',
|
||||
description: 'Kyrgyzstani Som',
|
||||
},
|
||||
{
|
||||
code: 'KHR',
|
||||
description: 'Cambodian Riel',
|
||||
},
|
||||
{
|
||||
code: 'KMF',
|
||||
description: 'Comorian Franc',
|
||||
},
|
||||
{
|
||||
code: 'KRW',
|
||||
description: 'South Korean Won',
|
||||
},
|
||||
{
|
||||
code: 'KYD',
|
||||
description: 'Cayman Islands Dollar',
|
||||
},
|
||||
{
|
||||
code: 'KZT',
|
||||
description: 'Kazakhstani Tenge',
|
||||
},
|
||||
{
|
||||
code: 'LAK',
|
||||
description: 'Lao Kip**',
|
||||
},
|
||||
{
|
||||
code: 'LBP',
|
||||
description: 'Lebanese Pound',
|
||||
},
|
||||
{
|
||||
code: 'LKR',
|
||||
description: 'Sri Lankan Rupee',
|
||||
},
|
||||
{
|
||||
code: 'LRD',
|
||||
description: 'Liberian Dollar',
|
||||
},
|
||||
{
|
||||
code: 'LSL',
|
||||
description: 'Lesotho Loti',
|
||||
},
|
||||
{
|
||||
code: 'MAD',
|
||||
description: 'Moroccan Dirham',
|
||||
},
|
||||
{
|
||||
code: 'MDL',
|
||||
description: 'Moldovan Leu',
|
||||
},
|
||||
{
|
||||
code: 'MGA',
|
||||
description: 'Malagasy Ariary',
|
||||
},
|
||||
{
|
||||
code: 'MKD',
|
||||
description: 'Macedonian Denar',
|
||||
},
|
||||
{
|
||||
code: 'MNT',
|
||||
description: 'Mongolian Tögrög',
|
||||
},
|
||||
{
|
||||
code: 'MOP',
|
||||
description: 'Macanese Pataca',
|
||||
},
|
||||
{
|
||||
code: 'MRO',
|
||||
description: 'Mauritanian Ouguiya',
|
||||
},
|
||||
{
|
||||
code: 'MUR',
|
||||
description: 'Mauritian Rupee**',
|
||||
},
|
||||
{
|
||||
code: 'MVR',
|
||||
description: 'Maldivian Rufiyaa',
|
||||
},
|
||||
{
|
||||
code: 'MWK',
|
||||
description: 'Malawian Kwacha',
|
||||
},
|
||||
{
|
||||
code: 'MXN',
|
||||
description: 'Mexican Peso**',
|
||||
},
|
||||
{
|
||||
code: 'MYR',
|
||||
description: 'Malaysian Ringgit',
|
||||
},
|
||||
{
|
||||
code: 'MZN',
|
||||
description: 'Mozambican Metical',
|
||||
},
|
||||
{
|
||||
code: 'NAD',
|
||||
description: 'Namibian Dollar',
|
||||
},
|
||||
{
|
||||
code: 'NGN',
|
||||
description: 'Nigerian Naira',
|
||||
},
|
||||
{
|
||||
code: 'NIO',
|
||||
description: 'Nicaraguan Córdoba**',
|
||||
},
|
||||
{
|
||||
code: 'NOK',
|
||||
description: 'Norwegian Krone',
|
||||
},
|
||||
{
|
||||
code: 'NPR',
|
||||
description: 'Nepalese Rupee',
|
||||
},
|
||||
{
|
||||
code: 'NZD',
|
||||
description: 'New Zealand Dollar',
|
||||
},
|
||||
{
|
||||
code: 'PAB',
|
||||
description: 'Panamanian Balboa**',
|
||||
},
|
||||
{
|
||||
code: 'PEN',
|
||||
description: 'Peruvian Nuevo Sol**',
|
||||
},
|
||||
{
|
||||
code: 'PGK',
|
||||
description: 'Papua New Guinean Kina',
|
||||
},
|
||||
{
|
||||
code: 'PHP',
|
||||
description: 'Philippine Peso',
|
||||
},
|
||||
{
|
||||
code: 'PKR',
|
||||
description: 'Pakistani Rupee',
|
||||
},
|
||||
{
|
||||
code: 'PLN',
|
||||
description: 'Polish Złoty',
|
||||
},
|
||||
{
|
||||
code: 'PYG',
|
||||
description: 'Paraguayan Guaraní**',
|
||||
},
|
||||
{
|
||||
code: 'QAR',
|
||||
description: 'Qatari Riyal',
|
||||
},
|
||||
{
|
||||
code: 'RON',
|
||||
description: 'Romanian Leu',
|
||||
},
|
||||
{
|
||||
code: 'RSD',
|
||||
description: 'Serbian Dinar',
|
||||
},
|
||||
{
|
||||
code: 'RUB',
|
||||
description: 'Russian Ruble',
|
||||
},
|
||||
{
|
||||
code: 'RWF',
|
||||
description: 'Rwandan Franc',
|
||||
},
|
||||
{
|
||||
code: 'SAR',
|
||||
description: 'Saudi Riyal',
|
||||
},
|
||||
{
|
||||
code: 'SBD',
|
||||
description: 'Solomon Islands Dollar',
|
||||
},
|
||||
{
|
||||
code: 'SCR',
|
||||
description: 'Seychellois Rupee',
|
||||
},
|
||||
{
|
||||
code: 'SEK',
|
||||
description: 'Swedish Krona',
|
||||
},
|
||||
{
|
||||
code: 'SGD',
|
||||
description: 'Singapore Dollar',
|
||||
},
|
||||
{
|
||||
code: 'SHP',
|
||||
description: 'Saint Helenian Pound**',
|
||||
},
|
||||
{
|
||||
code: 'SLL',
|
||||
description: 'Sierra Leonean Leone',
|
||||
},
|
||||
{
|
||||
code: 'SOS',
|
||||
description: 'Somali Shilling',
|
||||
},
|
||||
{
|
||||
code: 'SRD',
|
||||
description: 'Surinamese Dollar**',
|
||||
},
|
||||
{
|
||||
code: 'STD',
|
||||
description: 'São Tomé and Príncipe Dobra',
|
||||
},
|
||||
{
|
||||
code: 'SVC',
|
||||
description: 'Salvadoran Colón**',
|
||||
},
|
||||
{
|
||||
code: 'SZL',
|
||||
description: 'Swazi Lilangeni',
|
||||
},
|
||||
{
|
||||
code: 'THB',
|
||||
description: 'Thai Baht',
|
||||
},
|
||||
{
|
||||
code: 'TJS',
|
||||
description: 'Tajikistani Somoni',
|
||||
},
|
||||
{
|
||||
code: 'TOP',
|
||||
description: 'Tongan Paʻanga',
|
||||
},
|
||||
{
|
||||
code: 'TRY',
|
||||
description: 'Turkish Lira',
|
||||
},
|
||||
{
|
||||
code: 'TTD',
|
||||
description: 'Trinidad and Tobago Dollar',
|
||||
},
|
||||
{
|
||||
code: 'TWD',
|
||||
description: 'New Taiwan Dollar',
|
||||
},
|
||||
{
|
||||
code: 'TZS',
|
||||
description: 'Tanzanian Shilling',
|
||||
},
|
||||
{
|
||||
code: 'UAH',
|
||||
description: 'Ukrainian Hryvnia',
|
||||
},
|
||||
{
|
||||
code: 'UGX',
|
||||
description: 'Ugandan Shilling',
|
||||
},
|
||||
{
|
||||
code: 'USD',
|
||||
description: 'United States Dollar',
|
||||
},
|
||||
{
|
||||
code: 'UYU',
|
||||
description: 'Uruguayan Peso**',
|
||||
},
|
||||
{
|
||||
code: 'UZS',
|
||||
description: 'Uzbekistani Som',
|
||||
},
|
||||
{
|
||||
code: 'VND',
|
||||
description: 'Vietnamese Đồng',
|
||||
},
|
||||
{
|
||||
code: 'VUV',
|
||||
description: 'Vanuatu Vatu',
|
||||
},
|
||||
{
|
||||
code: 'WST',
|
||||
description: 'Samoan Tala',
|
||||
},
|
||||
{
|
||||
code: 'XAF',
|
||||
description: 'Central African Cfa Franc',
|
||||
},
|
||||
{
|
||||
code: 'XCD',
|
||||
description: 'East Caribbean Dollar',
|
||||
},
|
||||
{
|
||||
code: 'XOF',
|
||||
description: 'West African Cfa Franc**',
|
||||
},
|
||||
{
|
||||
code: 'XPF',
|
||||
description: 'Cfp Franc**',
|
||||
},
|
||||
{
|
||||
code: 'YER',
|
||||
description: 'Yemeni Rial',
|
||||
},
|
||||
{
|
||||
code: 'ZAR',
|
||||
description: 'South African Rand',
|
||||
},
|
||||
{
|
||||
code: 'ZMW',
|
||||
description: 'Zambian Kwacha',
|
||||
},
|
||||
]
|
@ -0,0 +1 @@
|
||||
export { PaymentSettings } from './PaymentSettings'
|
@ -0,0 +1,235 @@
|
||||
import { Select } from '@chakra-ui/react'
|
||||
import React, { ChangeEvent } from 'react'
|
||||
|
||||
type Props = {
|
||||
countryCode?: string
|
||||
onSelect: (countryCode: string) => void
|
||||
}
|
||||
|
||||
export const CountryCodeSelect = ({ countryCode, onSelect }: Props) => {
|
||||
const handleOnChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
onSelect(e.target.value)
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
placeholder="International"
|
||||
value={countryCode}
|
||||
onChange={handleOnChange}
|
||||
>
|
||||
<option value="DZ">Algeria (+213)</option>
|
||||
<option value="AD">Andorra (+376)</option>
|
||||
<option value="AO">Angola (+244)</option>
|
||||
<option value="AI">Anguilla (+1264)</option>
|
||||
<option value="AG">Antigua & Barbuda (+1268)</option>
|
||||
<option value="AR">Argentina (+54)</option>
|
||||
<option value="AM">Armenia (+374)</option>
|
||||
<option value="AW">Aruba (+297)</option>
|
||||
<option value="AU">Australia (+61)</option>
|
||||
<option value="AT">Austria (+43)</option>
|
||||
<option value="AZ">Azerbaijan (+994)</option>
|
||||
<option value="BS">Bahamas (+1242)</option>
|
||||
<option value="BH">Bahrain (+973)</option>
|
||||
<option value="BD">Bangladesh (+880)</option>
|
||||
<option value="BB">Barbados (+1246)</option>
|
||||
<option value="BY">Belarus (+375)</option>
|
||||
<option value="BE">Belgium (+32)</option>
|
||||
<option value="BZ">Belize (+501)</option>
|
||||
<option value="BJ">Benin (+229)</option>
|
||||
<option value="BM">Bermuda (+1441)</option>
|
||||
<option value="BT">Bhutan (+975)</option>
|
||||
<option value="BO">Bolivia (+591)</option>
|
||||
<option value="BA">Bosnia Herzegovina (+387)</option>
|
||||
<option value="BW">Botswana (+267)</option>
|
||||
<option value="BR">Brazil (+55)</option>
|
||||
<option value="BN">Brunei (+673)</option>
|
||||
<option value="BG">Bulgaria (+359)</option>
|
||||
<option value="BF">Burkina Faso (+226)</option>
|
||||
<option value="BI">Burundi (+257)</option>
|
||||
<option value="KH">Cambodia (+855)</option>
|
||||
<option value="CM">Cameroon (+237)</option>
|
||||
<option value="CA">Canada (+1)</option>
|
||||
<option value="CV">Cape Verde Islands (+238)</option>
|
||||
<option value="KY">Cayman Islands (+1345)</option>
|
||||
<option value="CF">Central African Republic (+236)</option>
|
||||
<option value="CL">Chile (+56)</option>
|
||||
<option value="CN">China (+86)</option>
|
||||
<option value="CO">Colombia (+57)</option>
|
||||
<option value="KM">Comoros (+269)</option>
|
||||
<option value="CG">Congo (+242)</option>
|
||||
<option value="CK">Cook Islands (+682)</option>
|
||||
<option value="CR">Costa Rica (+506)</option>
|
||||
<option value="HR">Croatia (+385)</option>
|
||||
<option value="CU">Cuba (+53)</option>
|
||||
<option value="CY">Cyprus North (+90392)</option>
|
||||
<option value="CY">Cyprus South (+357)</option>
|
||||
<option value="CZ">Czech Republic (+42)</option>
|
||||
<option value="DK">Denmark (+45)</option>
|
||||
<option value="DJ">Djibouti (+253)</option>
|
||||
<option value="DM">Dominica (+1809)</option>
|
||||
<option value="DO">Dominican Republic (+1809)</option>
|
||||
<option value="EC">Ecuador (+593)</option>
|
||||
<option value="EG">Egypt (+20)</option>
|
||||
<option value="SV">El Salvador (+503)</option>
|
||||
<option value="GQ">Equatorial Guinea (+240)</option>
|
||||
<option value="ER">Eritrea (+291)</option>
|
||||
<option value="EE">Estonia (+372)</option>
|
||||
<option value="ET">Ethiopia (+251)</option>
|
||||
<option value="FK">Falkland Islands (+500)</option>
|
||||
<option value="FO">Faroe Islands (+298)</option>
|
||||
<option value="FJ">Fiji (+679)</option>
|
||||
<option value="FI">Finland (+358)</option>
|
||||
<option value="FR">France (+33)</option>
|
||||
<option value="GF">French Guiana (+594)</option>
|
||||
<option value="PF">French Polynesia (+689)</option>
|
||||
<option value="GA">Gabon (+241)</option>
|
||||
<option value="GM">Gambia (+220)</option>
|
||||
<option value="GE">Georgia (+7880)</option>
|
||||
<option value="DE">Germany (+49)</option>
|
||||
<option value="GH">Ghana (+233)</option>
|
||||
<option value="GI">Gibraltar (+350)</option>
|
||||
<option value="GR">Greece (+30)</option>
|
||||
<option value="GL">Greenland (+299)</option>
|
||||
<option value="GD">Grenada (+1473)</option>
|
||||
<option value="GP">Guadeloupe (+590)</option>
|
||||
<option value="GU">Guam (+671)</option>
|
||||
<option value="GT">Guatemala (+502)</option>
|
||||
<option value="GN">Guinea (+224)</option>
|
||||
<option value="GW">Guinea - Bissau (+245)</option>
|
||||
<option value="GY">Guyana (+592)</option>
|
||||
<option value="HT">Haiti (+509)</option>
|
||||
<option value="HN">Honduras (+504)</option>
|
||||
<option value="HK">Hong Kong (+852)</option>
|
||||
<option value="HU">Hungary (+36)</option>
|
||||
<option value="IS">Iceland (+354)</option>
|
||||
<option value="IN">India (+91)</option>
|
||||
<option value="ID">Indonesia (+62)</option>
|
||||
<option value="IR">Iran (+98)</option>
|
||||
<option value="IQ">Iraq (+964)</option>
|
||||
<option value="IE">Ireland (+353)</option>
|
||||
<option value="IL">Israel (+972)</option>
|
||||
<option value="IT">Italy (+39)</option>
|
||||
<option value="JM">Jamaica (+1876)</option>
|
||||
<option value="JP">Japan (+81)</option>
|
||||
<option value="JO">Jordan (+962)</option>
|
||||
<option value="KZ">Kazakhstan (+7)</option>
|
||||
<option value="KE">Kenya (+254)</option>
|
||||
<option value="KI">Kiribati (+686)</option>
|
||||
<option value="KP">Korea North (+850)</option>
|
||||
<option value="KR">Korea South (+82)</option>
|
||||
<option value="KW">Kuwait (+965)</option>
|
||||
<option value="KG">Kyrgyzstan (+996)</option>
|
||||
<option value="LA">Laos (+856)</option>
|
||||
<option value="LV">Latvia (+371)</option>
|
||||
<option value="LB">Lebanon (+961)</option>
|
||||
<option value="LS">Lesotho (+266)</option>
|
||||
<option value="LR">Liberia (+231)</option>
|
||||
<option value="LY">Libya (+218)</option>
|
||||
<option value="LI">Liechtenstein (+417)</option>
|
||||
<option value="LT">Lithuania (+370)</option>
|
||||
<option value="LU">Luxembourg (+352)</option>
|
||||
<option value="MO">Macao (+853)</option>
|
||||
<option value="MK">Macedonia (+389)</option>
|
||||
<option value="MG">Madagascar (+261)</option>
|
||||
<option value="MW">Malawi (+265)</option>
|
||||
<option value="MY">Malaysia (+60)</option>
|
||||
<option value="MV">Maldives (+960)</option>
|
||||
<option value="ML">Mali (+223)</option>
|
||||
<option value="MT">Malta (+356)</option>
|
||||
<option value="MH">Marshall Islands (+692)</option>
|
||||
<option value="MQ">Martinique (+596)</option>
|
||||
<option value="MR">Mauritania (+222)</option>
|
||||
<option value="YT">Mayotte (+269)</option>
|
||||
<option value="MX">Mexico (+52)</option>
|
||||
<option value="FM">Micronesia (+691)</option>
|
||||
<option value="MD">Moldova (+373)</option>
|
||||
<option value="MC">Monaco (+377)</option>
|
||||
<option value="MN">Mongolia (+976)</option>
|
||||
<option value="MS">Montserrat (+1664)</option>
|
||||
<option value="MA">Morocco (+212)</option>
|
||||
<option value="MZ">Mozambique (+258)</option>
|
||||
<option value="MN">Myanmar (+95)</option>
|
||||
<option value="NA">Namibia (+264)</option>
|
||||
<option value="NR">Nauru (+674)</option>
|
||||
<option value="NP">Nepal (+977)</option>
|
||||
<option value="NL">Netherlands (+31)</option>
|
||||
<option value="NC">New Caledonia (+687)</option>
|
||||
<option value="NZ">New Zealand (+64)</option>
|
||||
<option value="NI">Nicaragua (+505)</option>
|
||||
<option value="NE">Niger (+227)</option>
|
||||
<option value="NG">Nigeria (+234)</option>
|
||||
<option value="NU">Niue (+683)</option>
|
||||
<option value="NF">Norfolk Islands (+672)</option>
|
||||
<option value="NP">Northern Marianas (+670)</option>
|
||||
<option value="NO">Norway (+47)</option>
|
||||
<option value="OM">Oman (+968)</option>
|
||||
<option value="PW">Palau (+680)</option>
|
||||
<option value="PA">Panama (+507)</option>
|
||||
<option value="PG">Papua New Guinea (+675)</option>
|
||||
<option value="PY">Paraguay (+595)</option>
|
||||
<option value="PE">Peru (+51)</option>
|
||||
<option value="PH">Philippines (+63)</option>
|
||||
<option value="PL">Poland (+48)</option>
|
||||
<option value="PT">Portugal (+351)</option>
|
||||
<option value="PR">Puerto Rico (+1787)</option>
|
||||
<option value="QA">Qatar (+974)</option>
|
||||
<option value="RE">Reunion (+262)</option>
|
||||
<option value="RO">Romania (+40)</option>
|
||||
<option value="RU">Russia (+7)</option>
|
||||
<option value="RW">Rwanda (+250)</option>
|
||||
<option value="SM">San Marino (+378)</option>
|
||||
<option value="ST">Sao Tome & Principe (+239)</option>
|
||||
<option value="SA">Saudi Arabia (+966)</option>
|
||||
<option value="SN">Senegal (+221)</option>
|
||||
<option value="CS">Serbia (+381)</option>
|
||||
<option value="SC">Seychelles (+248)</option>
|
||||
<option value="SL">Sierra Leone (+232)</option>
|
||||
<option value="SG">Singapore (+65)</option>
|
||||
<option value="SK">Slovak Republic (+421)</option>
|
||||
<option value="SI">Slovenia (+386)</option>
|
||||
<option value="SB">Solomon Islands (+677)</option>
|
||||
<option value="SO">Somalia (+252)</option>
|
||||
<option value="ZA">South Africa (+27)</option>
|
||||
<option value="ES">Spain (+34)</option>
|
||||
<option value="LK">Sri Lanka (+94)</option>
|
||||
<option value="SH">St. Helena (+290)</option>
|
||||
<option value="KN">St. Kitts (+1869)</option>
|
||||
<option value="SC">St. Lucia (+1758)</option>
|
||||
<option value="SD">Sudan (+249)</option>
|
||||
<option value="SR">Suriname (+597)</option>
|
||||
<option value="SZ">Swaziland (+268)</option>
|
||||
<option value="SE">Sweden (+46)</option>
|
||||
<option value="CH">Switzerland (+41)</option>
|
||||
<option value="SI">Syria (+963)</option>
|
||||
<option value="TW">Taiwan (+886)</option>
|
||||
<option value="TJ">Tajikstan (+7)</option>
|
||||
<option value="TH">Thailand (+66)</option>
|
||||
<option value="TG">Togo (+228)</option>
|
||||
<option value="TO">Tonga (+676)</option>
|
||||
<option value="TT">Trinidad & Tobago (+1868)</option>
|
||||
<option value="TN">Tunisia (+216)</option>
|
||||
<option value="TR">Turkey (+90)</option>
|
||||
<option value="TM">Turkmenistan (+7)</option>
|
||||
<option value="TM">Turkmenistan (+993)</option>
|
||||
<option value="TC">Turks & Caicos Islands (+1649)</option>
|
||||
<option value="TV">Tuvalu (+688)</option>
|
||||
<option value="UG">Uganda (+256)</option>
|
||||
<option value="GB">UK (+44)</option>
|
||||
<option value="UA">Ukraine (+380)</option>
|
||||
<option value="AE">United Arab Emirates (+971)</option>
|
||||
<option value="UY">Uruguay (+598)</option>
|
||||
<option value="US">USA (+1)</option>
|
||||
<option value="UZ">Uzbekistan (+7)</option>
|
||||
<option value="VU">Vanuatu (+678)</option>
|
||||
<option value="VA">Vatican City (+379)</option>
|
||||
<option value="VE">Venezuela (+58)</option>
|
||||
<option value="VN">Vietnam (+84)</option>
|
||||
<option value="VG">Virgin Islands - British (+1284)</option>
|
||||
<option value="VI">Virgin Islands - US (+1340)</option>
|
||||
<option value="WF">Wallis & Futuna (+681)</option>
|
||||
<option value="YE">Yemen (North)(+969)</option>
|
||||
<option value="YE">Yemen (South)(+967)</option>
|
||||
<option value="ZM">Zambia (+260)</option>
|
||||
<option value="ZW">Zimbabwe (+263)</option>
|
||||
</Select>
|
||||
)
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { PhoneNumberInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
import { CountryCodeSelect } from './CountryCodeSelect'
|
||||
|
||||
type PhoneNumberSettingsBodyProps = {
|
||||
options: PhoneNumberInputOptions
|
||||
onOptionsChange: (options: PhoneNumberInputOptions) => void
|
||||
}
|
||||
|
||||
export const PhoneNumberSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: PhoneNumberSettingsBodyProps) => {
|
||||
const handlePlaceholderChange = (placeholder: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
const handleRetryMessageChange = (retryMessageContent: string) =>
|
||||
onOptionsChange({ ...options, retryMessageContent })
|
||||
const handleDefaultCountryChange = (defaultCountryCode: string) =>
|
||||
onOptionsChange({ ...options, defaultCountryCode })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="placeholder">
|
||||
Placeholder:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="placeholder"
|
||||
defaultValue={options.labels.placeholder}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.button}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Default country:
|
||||
</FormLabel>
|
||||
<CountryCodeSelect
|
||||
onSelect={handleDefaultCountryChange}
|
||||
countryCode={options.defaultCountryCode}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="retry">
|
||||
Retry message:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="retry"
|
||||
defaultValue={options.retryMessageContent}
|
||||
onChange={handleRetryMessageChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { PhoneNumberSettingsBody } from './PhoneNumberSettingsBody'
|
@ -0,0 +1,149 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { RatingInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type RatingInputSettingsProps = {
|
||||
options: RatingInputOptions
|
||||
onOptionsChange: (options: RatingInputOptions) => void
|
||||
}
|
||||
|
||||
export const RatingInputSettings = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: RatingInputSettingsProps) => {
|
||||
const handleLengthChange = (length: number) =>
|
||||
onOptionsChange({ ...options, length })
|
||||
|
||||
const handleTypeChange = (buttonType: 'Icons' | 'Numbers') =>
|
||||
onOptionsChange({ ...options, buttonType })
|
||||
|
||||
const handleCustomIconCheck = (isEnabled: boolean) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
customIcon: { ...options.customIcon, isEnabled },
|
||||
})
|
||||
|
||||
const handleIconSvgChange = (svg: string) =>
|
||||
onOptionsChange({ ...options, customIcon: { ...options.customIcon, svg } })
|
||||
|
||||
const handleLeftLabelChange = (left: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, left } })
|
||||
|
||||
const handleMiddleLabelChange = (middle: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, middle } })
|
||||
|
||||
const handleRightLabelChange = (right: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, right } })
|
||||
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Maximum:
|
||||
</FormLabel>
|
||||
<DropdownList
|
||||
onItemSelect={handleLengthChange}
|
||||
items={[3, 4, 5, 6, 7, 8, 9, 10]}
|
||||
currentItem={options.length}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Type:
|
||||
</FormLabel>
|
||||
<DropdownList
|
||||
onItemSelect={handleTypeChange}
|
||||
items={['Icons', 'Numbers']}
|
||||
currentItem={options.buttonType}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{options.buttonType === 'Icons' && (
|
||||
<SwitchWithLabel
|
||||
id="switch"
|
||||
label="Custom icon?"
|
||||
initialValue={options.customIcon.isEnabled}
|
||||
onCheckChange={handleCustomIconCheck}
|
||||
/>
|
||||
)}
|
||||
{options.buttonType === 'Icons' && options.customIcon.isEnabled && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="svg">
|
||||
Icon SVG:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="svg"
|
||||
defaultValue={options.customIcon.svg}
|
||||
onChange={handleIconSvgChange}
|
||||
placeholder="<svg>...</svg>"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
1 label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.left}
|
||||
onChange={handleLeftLabelChange}
|
||||
placeholder="Not likely at all"
|
||||
/>
|
||||
</Stack>
|
||||
{options.length >= 4 && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
{Math.floor(options.length / 2)} label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.middle}
|
||||
onChange={handleMiddleLabelChange}
|
||||
placeholder="Neutral"
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
{options.length} label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.right}
|
||||
onChange={handleRightLabelChange}
|
||||
placeholder="Extremely likely"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.button}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { RedirectOptions } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
options: RedirectOptions
|
||||
onOptionsChange: (options: RedirectOptions) => void
|
||||
}
|
||||
|
||||
export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const handleUrlChange = (url?: string) => onOptionsChange({ ...options, url })
|
||||
|
||||
const handleIsNewTabChange = (isNewTab: boolean) =>
|
||||
onOptionsChange({ ...options, isNewTab })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="tracking-id">
|
||||
Url:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="tracking-id"
|
||||
defaultValue={options.url ?? ''}
|
||||
placeholder="Type a URL..."
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
</Stack>
|
||||
<SwitchWithLabel
|
||||
id="new-tab"
|
||||
label="Open in new tab?"
|
||||
initialValue={options.isNewTab}
|
||||
onCheckChange={handleIsNewTabChange}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
import { Stack, useDisclosure, Text } from '@chakra-ui/react'
|
||||
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||
import { Input, Textarea } from 'components/shared/Textbox'
|
||||
import { CredentialsType, SendEmailOptions } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { SmtpConfigModal } from './SmtpConfigModal'
|
||||
|
||||
type Props = {
|
||||
options: SendEmailOptions
|
||||
onOptionsChange: (options: SendEmailOptions) => void
|
||||
}
|
||||
|
||||
export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [refreshCredentialsKey, setRefreshCredentialsKey] = useState(0)
|
||||
|
||||
const handleCredentialsSelect = (credentialsId?: string) => {
|
||||
setRefreshCredentialsKey(refreshCredentialsKey + 1)
|
||||
onOptionsChange({
|
||||
...options,
|
||||
credentialsId: credentialsId === undefined ? 'default' : credentialsId,
|
||||
})
|
||||
}
|
||||
|
||||
const handleToChange = (recipientsStr: string) => {
|
||||
const recipients: string[] = recipientsStr
|
||||
.split(',')
|
||||
.map((str) => str.trim())
|
||||
onOptionsChange({
|
||||
...options,
|
||||
recipients,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCcChange = (ccStr: string) => {
|
||||
const cc: string[] = ccStr.split(',').map((str) => str.trim())
|
||||
onOptionsChange({
|
||||
...options,
|
||||
cc,
|
||||
})
|
||||
}
|
||||
|
||||
const handleBccChange = (bccStr: string) => {
|
||||
const bcc: string[] = bccStr.split(',').map((str) => str.trim())
|
||||
onOptionsChange({
|
||||
...options,
|
||||
bcc,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubjectChange = (subject: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
subject,
|
||||
})
|
||||
|
||||
const handleBodyChange = (body: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
body,
|
||||
})
|
||||
|
||||
const handleReplyToChange = (replyTo: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
replyTo,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<Text>From: </Text>
|
||||
<CredentialsDropdown
|
||||
type={CredentialsType.SMTP}
|
||||
currentCredentialsId={options.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsSelect}
|
||||
onCreateNewClick={onOpen}
|
||||
defaultCredentialLabel={process.env.NEXT_PUBLIC_SMTP_FROM?.match(
|
||||
/\<(.*)\>/
|
||||
)?.pop()}
|
||||
refreshDropdownKey={refreshCredentialsKey}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Reply to: </Text>
|
||||
<Input
|
||||
onChange={handleReplyToChange}
|
||||
defaultValue={options.replyTo}
|
||||
placeholder={'email@gmail.com'}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>To: </Text>
|
||||
<Input
|
||||
onChange={handleToChange}
|
||||
defaultValue={options.recipients.join(', ')}
|
||||
placeholder="email1@gmail.com, email2@gmail.com"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Cc: </Text>
|
||||
<Input
|
||||
onChange={handleCcChange}
|
||||
defaultValue={options.cc?.join(', ') ?? ''}
|
||||
placeholder="email1@gmail.com, email2@gmail.com"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Bcc: </Text>
|
||||
<Input
|
||||
onChange={handleBccChange}
|
||||
defaultValue={options.bcc?.join(', ') ?? ''}
|
||||
placeholder="email1@gmail.com, email2@gmail.com"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Subject: </Text>
|
||||
<Input
|
||||
data-testid="subject-input"
|
||||
onChange={handleSubjectChange}
|
||||
defaultValue={options.subject ?? ''}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Body: </Text>
|
||||
<Textarea
|
||||
data-testid="body-input"
|
||||
minH="300px"
|
||||
onChange={handleBodyChange}
|
||||
defaultValue={options.body ?? ''}
|
||||
/>
|
||||
</Stack>
|
||||
<SmtpConfigModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onNewCredentials={handleCredentialsSelect}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import { FormControl, FormLabel, HStack, Stack } from '@chakra-ui/react'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { SmartNumberInput } from 'components/shared/SmartNumberInput'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { SmtpCredentialsData } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
config: SmtpCredentialsData
|
||||
onConfigChange: (config: SmtpCredentialsData) => void
|
||||
}
|
||||
|
||||
export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
|
||||
const handleFromEmailChange = (email: string) =>
|
||||
onConfigChange({ ...config, from: { ...config.from, email } })
|
||||
const handleFromNameChange = (name: string) =>
|
||||
onConfigChange({ ...config, from: { ...config.from, name } })
|
||||
const handleHostChange = (host: string) => onConfigChange({ ...config, host })
|
||||
const handleUsernameChange = (username: string) =>
|
||||
onConfigChange({ ...config, username })
|
||||
const handlePasswordChange = (password: string) =>
|
||||
onConfigChange({ ...config, password })
|
||||
const handleTlsCheck = (isTlsEnabled: boolean) =>
|
||||
onConfigChange({ ...config, isTlsEnabled })
|
||||
const handlePortNumberChange = (port?: number) =>
|
||||
isDefined(port) && onConfigChange({ ...config, port })
|
||||
|
||||
return (
|
||||
<Stack as="form" spacing={4}>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>From email:</FormLabel>
|
||||
<Input
|
||||
defaultValue={config.from.email ?? ''}
|
||||
onChange={handleFromEmailChange}
|
||||
placeholder="notifications@provider.com"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>From name:</FormLabel>
|
||||
<Input
|
||||
defaultValue={config.from.name ?? ''}
|
||||
onChange={handleFromNameChange}
|
||||
placeholder="John Smith"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Host:</FormLabel>
|
||||
<Input
|
||||
defaultValue={config.host ?? ''}
|
||||
onChange={handleHostChange}
|
||||
placeholder="mail.provider.com"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Username / Email:</FormLabel>
|
||||
<Input
|
||||
type="email"
|
||||
defaultValue={config.username ?? ''}
|
||||
onChange={handleUsernameChange}
|
||||
placeholder="user@provider.com"
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Password:</FormLabel>
|
||||
<Input
|
||||
type="password"
|
||||
defaultValue={config.password ?? ''}
|
||||
onChange={handlePasswordChange}
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</FormControl>
|
||||
<SwitchWithLabel
|
||||
id="Tls"
|
||||
label={'Use TLS?'}
|
||||
initialValue={config.isTlsEnabled ?? false}
|
||||
onCheckChange={handleTlsCheck}
|
||||
/>
|
||||
<FormControl as={HStack} justifyContent="space-between">
|
||||
<FormLabel mb="0">Port number:</FormLabel>
|
||||
<SmartNumberInput
|
||||
placeholder="25"
|
||||
value={config.port}
|
||||
onValueChange={handlePortNumberChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@chakra-ui/react'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { CredentialsType, SmtpCredentialsData } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { createCredentials } from 'services/user'
|
||||
import { testSmtpConfig } from 'services/integrations'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { SmtpConfigForm } from './SmtpConfigForm'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onNewCredentials: (id: string) => void
|
||||
}
|
||||
|
||||
export const SmtpConfigModal = ({
|
||||
isOpen,
|
||||
onNewCredentials,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const { showToast } = useToast()
|
||||
const [smtpConfig, setSmtpConfig] = useState<SmtpCredentialsData>({
|
||||
from: {},
|
||||
port: 25,
|
||||
})
|
||||
|
||||
const handleCreateClick = async () => {
|
||||
if (!user?.email || !workspace?.id) return
|
||||
setIsCreating(true)
|
||||
const { error: testSmtpError } = await testSmtpConfig(
|
||||
smtpConfig,
|
||||
user.email
|
||||
)
|
||||
if (testSmtpError) {
|
||||
setIsCreating(false)
|
||||
return showToast({
|
||||
title: 'Invalid configuration',
|
||||
description: "We couldn't send the test email with your configuration",
|
||||
})
|
||||
}
|
||||
const { data, error } = await createCredentials({
|
||||
data: smtpConfig,
|
||||
name: smtpConfig.from.email as string,
|
||||
type: CredentialsType.SMTP,
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
setIsCreating(false)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
if (!data?.credentials)
|
||||
return showToast({ description: "Credentials wasn't created" })
|
||||
onNewCredentials(data.credentials.id)
|
||||
onClose()
|
||||
}
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Create SMTP config</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<SmtpConfigForm config={smtpConfig} onConfigChange={setSmtpConfig} />
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={handleCreateClick}
|
||||
isDisabled={
|
||||
isNotDefined(smtpConfig.from.email) ||
|
||||
isNotDefined(smtpConfig.from.name) ||
|
||||
isNotDefined(smtpConfig.host) ||
|
||||
isNotDefined(smtpConfig.username) ||
|
||||
isNotDefined(smtpConfig.password)
|
||||
}
|
||||
isLoading={isCreating}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { SendEmailSettings } from './SendEmailSettings'
|
@ -0,0 +1,68 @@
|
||||
import { FormLabel, HStack, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||
import { Textarea } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { SetVariableOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
options: SetVariableOptions
|
||||
onOptionsChange: (options: SetVariableOptions) => void
|
||||
}
|
||||
|
||||
export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
const handleExpressionChange = (expressionToEvaluate: string) =>
|
||||
onOptionsChange({ ...options, expressionToEvaluate })
|
||||
const handleValueTypeChange = () =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
isCode: options.isCode ? !options.isCode : true,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable-search">
|
||||
Search or create variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
onSelectVariable={handleVariableChange}
|
||||
initialVariableId={options.variableId}
|
||||
id="variable-search"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<HStack justify="space-between">
|
||||
<FormLabel mb="0" htmlFor="expression">
|
||||
Value:
|
||||
</FormLabel>
|
||||
<HStack>
|
||||
<Text fontSize="sm">Text</Text>
|
||||
<Switch
|
||||
size="sm"
|
||||
isChecked={options.isCode ?? false}
|
||||
onChange={handleValueTypeChange}
|
||||
/>
|
||||
<Text fontSize="sm">Code</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{options.isCode ?? false ? (
|
||||
<CodeEditor
|
||||
value={options.expressionToEvaluate ?? ''}
|
||||
onChange={handleExpressionChange}
|
||||
lang="js"
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
id="expression"
|
||||
defaultValue={options.expressionToEvaluate ?? ''}
|
||||
onChange={handleExpressionChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { TextInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type TextInputSettingsBodyProps = {
|
||||
options: TextInputOptions
|
||||
onOptionsChange: (options: TextInputOptions) => void
|
||||
}
|
||||
|
||||
export const TextInputSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: TextInputSettingsBodyProps) => {
|
||||
const handlePlaceholderChange = (placeholder: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||
const handleLongChange = (isLong: boolean) =>
|
||||
onOptionsChange({ ...options, isLong })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<SwitchWithLabel
|
||||
id="switch"
|
||||
label="Long text?"
|
||||
initialValue={options?.isLong ?? false}
|
||||
onCheckChange={handleLongChange}
|
||||
/>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="placeholder">
|
||||
Placeholder:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="placeholder"
|
||||
defaultValue={options.labels.placeholder}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.button}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { Input } from '@chakra-ui/react'
|
||||
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
|
||||
import { Group } from 'models'
|
||||
import { useMemo } from 'react'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
groups: Group[]
|
||||
groupId?: string
|
||||
onGroupIdSelected: (groupId: string) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export const GroupsDropdown = ({
|
||||
groups,
|
||||
groupId,
|
||||
onGroupIdSelected,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
const currentGroup = useMemo(
|
||||
() => groups?.find(byId(groupId)),
|
||||
[groupId, groups]
|
||||
)
|
||||
|
||||
const handleGroupSelect = (title: string) => {
|
||||
const id = groups?.find((b) => b.title === title)?.id
|
||||
if (id) onGroupIdSelected(id)
|
||||
}
|
||||
|
||||
if (isLoading) return <Input value="Loading..." isDisabled />
|
||||
if (!groups || groups.length === 0)
|
||||
return <Input value="No groups found" isDisabled />
|
||||
return (
|
||||
<SearchableDropdown
|
||||
selectedItem={currentGroup?.title}
|
||||
items={(groups ?? []).map((b) => b.title)}
|
||||
onValueChange={handleGroupSelect}
|
||||
placeholder={'Select a block'}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { TypebotLinkOptions } from 'models'
|
||||
import { byId } from 'utils'
|
||||
import { GroupsDropdown } from './GroupsDropdown'
|
||||
import { TypebotsDropdown } from './TypebotsDropdown'
|
||||
|
||||
type Props = {
|
||||
options: TypebotLinkOptions
|
||||
onOptionsChange: (options: TypebotLinkOptions) => void
|
||||
}
|
||||
|
||||
export const TypebotLinkSettingsForm = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const { linkedTypebots, typebot } = useTypebot()
|
||||
|
||||
const handleTypebotIdChange = (typebotId: string | 'current') =>
|
||||
onOptionsChange({ ...options, typebotId })
|
||||
const handleGroupIdChange = (groupId: string) =>
|
||||
onOptionsChange({ ...options, groupId })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{typebot && (
|
||||
<TypebotsDropdown
|
||||
typebotId={options.typebotId}
|
||||
onSelectTypebotId={handleTypebotIdChange}
|
||||
currentWorkspaceId={typebot.workspaceId as string}
|
||||
/>
|
||||
)}
|
||||
<GroupsDropdown
|
||||
groups={
|
||||
typebot &&
|
||||
(options.typebotId === typebot.id || options.typebotId === 'current')
|
||||
? typebot.groups
|
||||
: linkedTypebots?.find(byId(options.typebotId))?.groups ?? []
|
||||
}
|
||||
groupId={options.groupId}
|
||||
onGroupIdSelected={handleGroupIdChange}
|
||||
isLoading={
|
||||
linkedTypebots === undefined &&
|
||||
typebot &&
|
||||
typebot.id !== options.typebotId
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import { HStack, IconButton, Input } from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from 'assets/icons'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
import { useTypebots } from 'services/typebots'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
typebotId?: string
|
||||
currentWorkspaceId: string
|
||||
onSelectTypebotId: (typebotId: string | 'current') => void
|
||||
}
|
||||
|
||||
export const TypebotsDropdown = ({
|
||||
typebotId,
|
||||
onSelectTypebotId,
|
||||
currentWorkspaceId,
|
||||
}: Props) => {
|
||||
const { query } = useRouter()
|
||||
const { showToast } = useToast()
|
||||
const { typebots, isLoading } = useTypebots({
|
||||
workspaceId: currentWorkspaceId,
|
||||
allFolders: true,
|
||||
onError: (e) => showToast({ title: e.name, description: e.message }),
|
||||
})
|
||||
const currentTypebot = useMemo(
|
||||
() => typebots?.find(byId(typebotId)),
|
||||
[typebotId, typebots]
|
||||
)
|
||||
|
||||
const handleTypebotSelect = (name: string) => {
|
||||
if (name === 'Current typebot') return onSelectTypebotId('current')
|
||||
const id = typebots?.find((s) => s.name === name)?.id
|
||||
if (id) onSelectTypebotId(id)
|
||||
}
|
||||
|
||||
if (isLoading) return <Input value="Loading..." isDisabled />
|
||||
if (!typebots || typebots.length === 0)
|
||||
return <Input value="No typebots found" isDisabled />
|
||||
return (
|
||||
<HStack>
|
||||
<SearchableDropdown
|
||||
selectedItem={currentTypebot?.name}
|
||||
items={['Current typebot', ...(typebots ?? []).map((t) => t.name)]}
|
||||
onValueChange={handleTypebotSelect}
|
||||
placeholder={'Select a typebot'}
|
||||
/>
|
||||
{currentTypebot?.id && (
|
||||
<IconButton
|
||||
aria-label="Navigate to typebot"
|
||||
icon={<ExternalLinkIcon />}
|
||||
as={NextChakraLink}
|
||||
href={`/typebots/${currentTypebot?.id}/edit?parentId=${query.typebotId}`}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { TypebotLinkSettingsForm } from './TypebotLinkSettingsForm'
|
@ -0,0 +1,68 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { UrlInputOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type UrlInputSettingsBodyProps = {
|
||||
options: UrlInputOptions
|
||||
onOptionsChange: (options: UrlInputOptions) => void
|
||||
}
|
||||
|
||||
export const UrlInputSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: UrlInputSettingsBodyProps) => {
|
||||
const handlePlaceholderChange = (placeholder: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
|
||||
const handleButtonLabelChange = (button: string) =>
|
||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
const handleRetryMessageChange = (retryMessageContent: string) =>
|
||||
onOptionsChange({ ...options, retryMessageContent })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="placeholder">
|
||||
Placeholder:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="placeholder"
|
||||
defaultValue={options.labels.placeholder}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="button"
|
||||
defaultValue={options.labels.button}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="retry">
|
||||
Retry message:
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="retry"
|
||||
defaultValue={options.retryMessageContent}
|
||||
onChange={handleRetryMessageChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="variable">
|
||||
Save answer in a variable:
|
||||
</FormLabel>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.variableId}
|
||||
onSelectVariable={handleVariableChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { TableListItemProps } from 'components/shared/TableList'
|
||||
import { KeyValue } from 'models'
|
||||
|
||||
export const QueryParamsInputs = (props: TableListItemProps<KeyValue>) => (
|
||||
<KeyValueInputs
|
||||
{...props}
|
||||
keyPlaceholder="e.g. email"
|
||||
valuePlaceholder="e.g. {{Email}}"
|
||||
/>
|
||||
)
|
||||
|
||||
export const HeadersInputs = (props: TableListItemProps<KeyValue>) => (
|
||||
<KeyValueInputs
|
||||
{...props}
|
||||
keyPlaceholder="e.g. Content-Type"
|
||||
valuePlaceholder="e.g. application/json"
|
||||
/>
|
||||
)
|
||||
|
||||
export const KeyValueInputs = ({
|
||||
item,
|
||||
onItemChange,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder,
|
||||
debounceTimeout,
|
||||
}: TableListItemProps<KeyValue> & {
|
||||
keyPlaceholder?: string
|
||||
valuePlaceholder?: string
|
||||
}) => {
|
||||
const handleKeyChange = (key: string) => {
|
||||
if (key === item.key) return
|
||||
onItemChange({ ...item, key })
|
||||
}
|
||||
const handleValueChange = (value: string) => {
|
||||
if (value === item.value) return
|
||||
onItemChange({ ...item, value })
|
||||
}
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'key' + item.id}>Key:</FormLabel>
|
||||
<Input
|
||||
id={'key' + item.id}
|
||||
defaultValue={item.key ?? ''}
|
||||
onChange={handleKeyChange}
|
||||
placeholder={keyPlaceholder}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'value' + item.id}>Value:</FormLabel>
|
||||
<Input
|
||||
id={'value' + item.id}
|
||||
defaultValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
placeholder={valuePlaceholder}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
|
||||
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
|
||||
import { TableListItemProps } from 'components/shared/TableList'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { Variable, ResponseVariableMapping } from 'models'
|
||||
|
||||
export const DataVariableInputs = ({
|
||||
item,
|
||||
onItemChange,
|
||||
dataItems,
|
||||
debounceTimeout,
|
||||
}: TableListItemProps<ResponseVariableMapping> & { dataItems: string[] }) => {
|
||||
const handleBodyPathChange = (bodyPath: string) =>
|
||||
onItemChange({ ...item, bodyPath })
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
onItemChange({ ...item, variableId: variable?.id })
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="name">Data:</FormLabel>
|
||||
<SearchableDropdown
|
||||
items={dataItems}
|
||||
value={item.bodyPath}
|
||||
onValueChange={handleBodyPathChange}
|
||||
placeholder="Select the data"
|
||||
debounceTimeout={debounceTimeout}
|
||||
withVariableButton
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="value">Set variable:</FormLabel>
|
||||
<VariableSearchInput
|
||||
onSelectVariable={handleVariableChange}
|
||||
placeholder="Search for a variable"
|
||||
initialVariableId={item.variableId}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { Stack, FormControl, FormLabel } from '@chakra-ui/react'
|
||||
import { TableListItemProps } from 'components/shared/TableList'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { VariableForTest, Variable } from 'models'
|
||||
|
||||
export const VariableForTestInputs = ({
|
||||
item,
|
||||
onItemChange,
|
||||
debounceTimeout,
|
||||
}: TableListItemProps<VariableForTest>) => {
|
||||
const handleVariableSelect = (variable?: Variable) =>
|
||||
onItemChange({ ...item, variableId: variable?.id })
|
||||
const handleValueChange = (value: string) => {
|
||||
if (value === item.value) return
|
||||
onItemChange({ ...item, value })
|
||||
}
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'name' + item.id}>Variable name:</FormLabel>
|
||||
<VariableSearchInput
|
||||
id={'name' + item.id}
|
||||
initialVariableId={item.variableId}
|
||||
onSelectVariable={handleVariableSelect}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'value' + item.id}>Test value:</FormLabel>
|
||||
<Input
|
||||
id={'value' + item.id}
|
||||
defaultValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,298 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Button,
|
||||
HStack,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Link,
|
||||
} from '@chakra-ui/react'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
HttpMethod,
|
||||
KeyValue,
|
||||
WebhookOptions,
|
||||
VariableForTest,
|
||||
ResponseVariableMapping,
|
||||
WebhookBlock,
|
||||
defaultWebhookAttributes,
|
||||
Webhook,
|
||||
MakeComBlock,
|
||||
PabblyConnectBlock,
|
||||
} from 'models'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
||||
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||
import {
|
||||
convertVariableForTestToVariables,
|
||||
executeWebhook,
|
||||
getDeepKeys,
|
||||
} from 'services/integrations'
|
||||
import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs'
|
||||
import { VariableForTestInputs } from './VariableForTestInputs'
|
||||
import { DataVariableInputs } from './ResponseMappingInputs'
|
||||
import { byId } from 'utils'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { ExternalLinkIcon } from 'assets/icons'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
|
||||
type Provider = {
|
||||
name: 'Make.com' | 'Pabbly Connect'
|
||||
url: string
|
||||
}
|
||||
type Props = {
|
||||
block: WebhookBlock | MakeComBlock | PabblyConnectBlock
|
||||
onOptionsChange: (options: WebhookOptions) => void
|
||||
provider?: Provider
|
||||
}
|
||||
|
||||
export const WebhookSettings = ({
|
||||
block: { options, id: blockId, webhookId },
|
||||
onOptionsChange,
|
||||
provider,
|
||||
}: Props) => {
|
||||
const { typebot, save, webhooks, updateWebhook } = useTypebot()
|
||||
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
|
||||
const [testResponse, setTestResponse] = useState<string>()
|
||||
const [responseKeys, setResponseKeys] = useState<string[]>([])
|
||||
|
||||
const { showToast } = useToast()
|
||||
const [localWebhook, setLocalWebhook] = useState(
|
||||
webhooks.find(byId(webhookId))
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (localWebhook) return
|
||||
const incomingWebhook = webhooks.find(byId(webhookId))
|
||||
setLocalWebhook(incomingWebhook)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [webhooks])
|
||||
|
||||
useEffect(() => {
|
||||
if (!typebot) return
|
||||
if (!localWebhook) {
|
||||
const newWebhook = {
|
||||
id: webhookId,
|
||||
...defaultWebhookAttributes,
|
||||
typebotId: typebot.id,
|
||||
} as Webhook
|
||||
updateWebhook(webhookId, newWebhook)
|
||||
}
|
||||
|
||||
return () => {
|
||||
setLocalWebhook((localWebhook) => {
|
||||
if (!localWebhook) return
|
||||
updateWebhook(webhookId, localWebhook).then()
|
||||
return localWebhook
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleUrlChange = (url?: string) =>
|
||||
localWebhook && setLocalWebhook({ ...localWebhook, url: url ?? null })
|
||||
|
||||
const handleMethodChange = (method: HttpMethod) =>
|
||||
localWebhook && setLocalWebhook({ ...localWebhook, method })
|
||||
|
||||
const handleQueryParamsChange = (queryParams: KeyValue[]) =>
|
||||
localWebhook && setLocalWebhook({ ...localWebhook, queryParams })
|
||||
|
||||
const handleHeadersChange = (headers: KeyValue[]) =>
|
||||
localWebhook && setLocalWebhook({ ...localWebhook, headers })
|
||||
|
||||
const handleBodyChange = (body: string) =>
|
||||
localWebhook && setLocalWebhook({ ...localWebhook, body })
|
||||
|
||||
const handleVariablesChange = (variablesForTest: VariableForTest[]) =>
|
||||
onOptionsChange({ ...options, variablesForTest })
|
||||
|
||||
const handleResponseMappingChange = (
|
||||
responseVariableMapping: ResponseVariableMapping[]
|
||||
) => onOptionsChange({ ...options, responseVariableMapping })
|
||||
|
||||
const handleAdvancedConfigChange = (isAdvancedConfig: boolean) =>
|
||||
onOptionsChange({ ...options, isAdvancedConfig })
|
||||
|
||||
const handleBodyFormStateChange = (isCustomBody: boolean) =>
|
||||
onOptionsChange({ ...options, isCustomBody })
|
||||
|
||||
const handleTestRequestClick = async () => {
|
||||
if (!typebot || !localWebhook) return
|
||||
setIsTestResponseLoading(true)
|
||||
await Promise.all([updateWebhook(localWebhook.id, localWebhook), save()])
|
||||
const { data, error } = await executeWebhook(
|
||||
typebot.id,
|
||||
convertVariableForTestToVariables(
|
||||
options.variablesForTest,
|
||||
typebot.variables
|
||||
),
|
||||
{ blockId }
|
||||
)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
setTestResponse(JSON.stringify(data, undefined, 2))
|
||||
setResponseKeys(getDeepKeys(data))
|
||||
setIsTestResponseLoading(false)
|
||||
}
|
||||
|
||||
const ResponseMappingInputs = useMemo(
|
||||
() => (props: TableListItemProps<ResponseVariableMapping>) =>
|
||||
<DataVariableInputs {...props} dataItems={responseKeys} />,
|
||||
[responseKeys]
|
||||
)
|
||||
|
||||
if (!localWebhook) return <Spinner />
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
{provider && (
|
||||
<Alert status={'info'} bgColor={'blue.50'} rounded="md">
|
||||
<AlertIcon />
|
||||
<Stack>
|
||||
<Text>Head up to {provider.name} to configure this block:</Text>
|
||||
<Button as={Link} href={provider.url} isExternal colorScheme="blue">
|
||||
<Text mr="2">{provider.name}</Text> <ExternalLinkIcon />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Alert>
|
||||
)}
|
||||
<Input
|
||||
placeholder="Paste webhook URL..."
|
||||
defaultValue={localWebhook.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
debounceTimeout={0}
|
||||
withVariableButton={!provider}
|
||||
/>
|
||||
<SwitchWithLabel
|
||||
id={'easy-config'}
|
||||
label="Advanced configuration"
|
||||
initialValue={options.isAdvancedConfig ?? true}
|
||||
onCheckChange={handleAdvancedConfigChange}
|
||||
/>
|
||||
{(options.isAdvancedConfig ?? true) && (
|
||||
<Stack>
|
||||
<HStack justify="space-between">
|
||||
<Text>Method:</Text>
|
||||
<DropdownList<HttpMethod>
|
||||
currentItem={localWebhook.method as HttpMethod}
|
||||
onItemSelect={handleMethodChange}
|
||||
items={Object.values(HttpMethod)}
|
||||
/>
|
||||
</HStack>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Query params
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={localWebhook.queryParams}
|
||||
onItemsChange={handleQueryParamsChange}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Headers
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={localWebhook.headers}
|
||||
onItemsChange={handleHeadersChange}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Body
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<SwitchWithLabel
|
||||
id={'custom-body'}
|
||||
label="Custom body"
|
||||
initialValue={options.isCustomBody ?? true}
|
||||
onCheckChange={handleBodyFormStateChange}
|
||||
/>
|
||||
{(options.isCustomBody ?? true) && (
|
||||
<CodeEditor
|
||||
value={localWebhook.body ?? ''}
|
||||
lang="json"
|
||||
onChange={handleBodyChange}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Variable values for test
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<VariableForTest>
|
||||
initialItems={
|
||||
options?.variablesForTest ?? { byId: {}, allIds: [] }
|
||||
}
|
||||
onItemsChange={handleVariablesChange}
|
||||
Item={VariableForTestInputs}
|
||||
addLabel="Add an entry"
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack>
|
||||
{localWebhook.url && (
|
||||
<Button
|
||||
onClick={handleTestRequestClick}
|
||||
colorScheme="blue"
|
||||
isLoading={isTestResponseLoading}
|
||||
>
|
||||
Test the request
|
||||
</Button>
|
||||
)}
|
||||
{testResponse && (
|
||||
<CodeEditor isReadOnly lang="json" value={testResponse} />
|
||||
)}
|
||||
{(testResponse || options?.responseVariableMapping.length > 0) && (
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton justifyContent="space-between">
|
||||
Save in variables
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={options.responseVariableMapping}
|
||||
onItemsChange={handleResponseMappingChange}
|
||||
Item={ResponseMappingInputs}
|
||||
addLabel="Add an entry"
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { WebhookSettings } from './WebhookSettings'
|
@ -0,0 +1,51 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Button,
|
||||
Input,
|
||||
Link,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ZapierBlock } from 'models'
|
||||
import React from 'react'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
block: ZapierBlock
|
||||
}
|
||||
|
||||
export const ZapierSettings = ({ block }: Props) => {
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = webhooks.find(byId(block.webhookId))
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Alert
|
||||
status={webhook?.url ? 'success' : 'info'}
|
||||
bgColor={webhook?.url ? undefined : 'blue.50'}
|
||||
rounded="md"
|
||||
>
|
||||
<AlertIcon />
|
||||
{webhook?.url ? (
|
||||
<>Your zap is correctly configured 🚀</>
|
||||
) : (
|
||||
<Stack>
|
||||
<Text>Head up to Zapier to configure this block:</Text>
|
||||
<Button
|
||||
as={Link}
|
||||
href="https://zapier.com/apps/typebot/integrations"
|
||||
isExternal
|
||||
colorScheme="blue"
|
||||
>
|
||||
<Text mr="2">Zapier</Text> <ExternalLinkIcon />
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Alert>
|
||||
{webhook?.url && <Input value={webhook?.url} isDisabled />}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
export * from './DateInputSettingsBody'
|
||||
export * from './EmailInputSettingsBody'
|
||||
export * from './NumberInputSettingsBody'
|
||||
export * from './TextInputSettingsBody'
|
||||
export * from './UrlInputSettingsBody'
|
Reference in New Issue
Block a user