@@ -52,7 +52,7 @@ test.describe.parallel('Buttons input block', () => {
|
||||
await page.getByLabel('Button label:').fill('Go')
|
||||
await page.getByPlaceholder('Select a variable').nth(1).click()
|
||||
await page.getByText('var1').click()
|
||||
await expect(page.getByText('Collectsvar1')).toBeVisible()
|
||||
await expect(page.getByText('Setvar1')).toBeVisible()
|
||||
await page.click('[data-testid="block2-icon"]')
|
||||
|
||||
await page.locator('text=Item 1').hover()
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { BlockIndices, ChoiceInputBlock, Variable } from 'models'
|
||||
import { BlockIndices, ChoiceInputBlock } from 'models'
|
||||
import React from 'react'
|
||||
import { ItemNodesList } from '@/features/graph/components/Nodes/ItemNode'
|
||||
import {
|
||||
HStack,
|
||||
Stack,
|
||||
Tag,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
Wrap,
|
||||
} from '@chakra-ui/react'
|
||||
import { Stack, Tag, Text, Wrap } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { SetVariableLabel } from '@/components/SetVariableLabel'
|
||||
|
||||
type Props = {
|
||||
block: ChoiceInputBlock
|
||||
@@ -25,7 +19,7 @@ export const ButtonsBlockNode = ({ block, indices }: Props) => {
|
||||
return (
|
||||
<Stack w="full">
|
||||
{block.options.variableId ? (
|
||||
<CollectVariableLabel
|
||||
<SetVariableLabel
|
||||
variableId={block.options.variableId}
|
||||
variables={typebot?.variables}
|
||||
/>
|
||||
@@ -44,28 +38,3 @@ export const ButtonsBlockNode = ({ block, indices }: Props) => {
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const CollectVariableLabel = ({
|
||||
variableId,
|
||||
variables,
|
||||
}: {
|
||||
variableId: string
|
||||
variables?: Variable[]
|
||||
}) => {
|
||||
const textColor = useColorModeValue('gray.600', 'gray.400')
|
||||
const variableName = variables?.find(
|
||||
(variable) => variable.id === variableId
|
||||
)?.name
|
||||
|
||||
if (!variableName) return null
|
||||
return (
|
||||
<HStack fontStyle="italic" spacing={1}>
|
||||
<Text fontSize="sm" color={textColor}>
|
||||
Collects
|
||||
</Text>
|
||||
<Tag bg="orange.400" color="white" size="sm">
|
||||
{variableName}
|
||||
</Tag>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
AccordionPanel,
|
||||
} from '@chakra-ui/react'
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { CredentialsType, PaymentInputOptions, PaymentProvider } from 'models'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { PaymentInputOptions, PaymentProvider } from 'models'
|
||||
import React, { ChangeEvent } from 'react'
|
||||
import { currencies } from './currencies'
|
||||
import { StripeConfigModal } from './StripeConfigModal'
|
||||
import { CredentialsDropdown } from '@/features/credentials'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
|
||||
type Props = {
|
||||
options: PaymentInputOptions
|
||||
@@ -24,8 +25,8 @@ type Props = {
|
||||
}
|
||||
|
||||
export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [refreshCredentialsKey, setRefreshCredentialsKey] = useState(0)
|
||||
|
||||
const handleProviderChange = (provider: PaymentProvider) => {
|
||||
onOptionsChange({
|
||||
@@ -35,7 +36,6 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
|
||||
}
|
||||
|
||||
const handleCredentialsSelect = (credentialsId?: string) => {
|
||||
setRefreshCredentialsKey(refreshCredentialsKey + 1)
|
||||
onOptionsChange({
|
||||
...options,
|
||||
credentialsId,
|
||||
@@ -96,13 +96,15 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text>Account:</Text>
|
||||
<CredentialsDropdown
|
||||
type={CredentialsType.STRIPE}
|
||||
currentCredentialsId={options.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsSelect}
|
||||
onCreateNewClick={onOpen}
|
||||
refreshDropdownKey={refreshCredentialsKey}
|
||||
/>
|
||||
{workspace && (
|
||||
<CredentialsDropdown
|
||||
type="stripe"
|
||||
workspaceId={workspace.id}
|
||||
currentCredentialsId={options.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsSelect}
|
||||
onCreateNewClick={onOpen}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<HStack>
|
||||
<TextInput
|
||||
|
||||
@@ -14,15 +14,15 @@ import {
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { useUser } from '@/features/account'
|
||||
import { CredentialsType, StripeCredentialsData } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { omit } from 'utils'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { createCredentialsQuery } from '@/features/credentials'
|
||||
import { StripeCredentials } from 'models'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { isNotEmpty } from 'utils'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
@@ -40,12 +40,32 @@ export const StripeConfigModal = ({
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const { showToast } = useToast()
|
||||
const [stripeConfig, setStripeConfig] = useState<
|
||||
StripeCredentialsData & { name: string }
|
||||
StripeCredentials['data'] & { name: string }
|
||||
>({
|
||||
name: '',
|
||||
live: { publicKey: '', secretKey: '' },
|
||||
test: { publicKey: '', secretKey: '' },
|
||||
})
|
||||
const {
|
||||
credentials: {
|
||||
listCredentials: { refetch: refetchCredentials },
|
||||
},
|
||||
} = trpc.useContext()
|
||||
const { mutate } = trpc.credentials.createCredentials.useMutation({
|
||||
onMutate: () => setIsCreating(true),
|
||||
onSettled: () => setIsCreating(false),
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
description: err.message,
|
||||
status: 'error',
|
||||
})
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
refetchCredentials()
|
||||
onNewCredentials(data.credentialsId)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
const handleNameChange = (name: string) =>
|
||||
setStripeConfig({
|
||||
@@ -77,22 +97,26 @@ export const StripeConfigModal = ({
|
||||
test: { ...stripeConfig.test, secretKey },
|
||||
})
|
||||
|
||||
const handleCreateClick = async () => {
|
||||
const createCredentials = async () => {
|
||||
if (!user?.email || !workspace?.id) return
|
||||
setIsCreating(true)
|
||||
const { data, error } = await createCredentialsQuery({
|
||||
data: omit(stripeConfig, 'name'),
|
||||
name: stripeConfig.name,
|
||||
type: CredentialsType.STRIPE,
|
||||
workspaceId: workspace.id,
|
||||
mutate({
|
||||
credentials: {
|
||||
data: {
|
||||
live: stripeConfig.live,
|
||||
test: {
|
||||
publicKey: isNotEmpty(stripeConfig.test.publicKey)
|
||||
? stripeConfig.test.publicKey
|
||||
: undefined,
|
||||
secretKey: isNotEmpty(stripeConfig.test.secretKey)
|
||||
? stripeConfig.test.secretKey
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
name: stripeConfig.name,
|
||||
type: '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}>
|
||||
@@ -108,6 +132,7 @@ export const StripeConfigModal = ({
|
||||
onChange={handleNameChange}
|
||||
placeholder="Typebot"
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
<Stack>
|
||||
<FormLabel>
|
||||
@@ -121,11 +146,13 @@ export const StripeConfigModal = ({
|
||||
onChange={handleTestPublicKeyChange}
|
||||
placeholder="pk_test_..."
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
<TextInput
|
||||
onChange={handleTestSecretKeyChange}
|
||||
placeholder="sk_test_..."
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</HStack>
|
||||
</Stack>
|
||||
@@ -137,6 +164,7 @@ export const StripeConfigModal = ({
|
||||
onChange={handlePublicKeyChange}
|
||||
placeholder="pk_live_..."
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
@@ -144,6 +172,7 @@ export const StripeConfigModal = ({
|
||||
onChange={handleSecretKeyChange}
|
||||
placeholder="sk_live_..."
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
@@ -162,7 +191,7 @@ export const StripeConfigModal = ({
|
||||
<ModalFooter>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={handleCreateClick}
|
||||
onClick={createCredentials}
|
||||
isDisabled={
|
||||
stripeConfig.live.publicKey === '' ||
|
||||
stripeConfig.name === '' ||
|
||||
|
||||
@@ -64,7 +64,7 @@ export const RatingInputSettings = ({
|
||||
</FormLabel>
|
||||
<DropdownList
|
||||
onItemSelect={handleTypeChange}
|
||||
items={['Icons', 'Numbers']}
|
||||
items={['Icons', 'Numbers'] as const}
|
||||
currentItem={options.buttonType}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const CellWithValueStack = ({
|
||||
}
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px" w="full">
|
||||
<DropdownList<string>
|
||||
<DropdownList
|
||||
currentItem={item.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const CellWithVariableIdStack = ({
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList<string>
|
||||
<DropdownList
|
||||
currentItem={item.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DropdownList } from '@/components/DropdownList'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import {
|
||||
Cell,
|
||||
CredentialsType,
|
||||
defaultGoogleSheetsGetOptions,
|
||||
defaultGoogleSheetsInsertOptions,
|
||||
defaultGoogleSheetsUpdateOptions,
|
||||
@@ -27,6 +26,7 @@ import { useSheets } from '../../hooks/useSheets'
|
||||
import { Sheet } from '../../types'
|
||||
import { RowsFilterTableList } from './RowsFilterTableList'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
|
||||
type Props = {
|
||||
options: GoogleSheetsOptions
|
||||
@@ -39,6 +39,7 @@ export const GoogleSheetsSettingsBody = ({
|
||||
onOptionsChange,
|
||||
blockId,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { save } = useTypebot()
|
||||
const { sheets, isLoading } = useSheets({
|
||||
credentialsId: options?.credentialsId,
|
||||
@@ -94,12 +95,15 @@ export const GoogleSheetsSettingsBody = ({
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CredentialsDropdown
|
||||
type={CredentialsType.GOOGLE_SHEETS}
|
||||
currentCredentialsId={options?.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsIdChange}
|
||||
onCreateNewClick={handleCreateNewClick}
|
||||
/>
|
||||
{workspace && (
|
||||
<CredentialsDropdown
|
||||
type="google sheets"
|
||||
workspaceId={workspace.id}
|
||||
currentCredentialsId={options?.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsIdChange}
|
||||
onCreateNewClick={handleCreateNewClick}
|
||||
/>
|
||||
)}
|
||||
<GoogleSheetConnectModal
|
||||
blockId={blockId}
|
||||
isOpen={isOpen}
|
||||
@@ -125,7 +129,7 @@ export const GoogleSheetsSettingsBody = ({
|
||||
isDefined(options.sheetId) && (
|
||||
<>
|
||||
<Divider />
|
||||
<DropdownList<GoogleSheetsAction>
|
||||
<DropdownList
|
||||
currentItem={'action' in options ? options.action : undefined}
|
||||
onItemSelect={handleActionChange}
|
||||
items={Object.values(GoogleSheetsAction)}
|
||||
|
||||
@@ -29,13 +29,13 @@ export const RowsFilterComparisonItem = ({
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList<string>
|
||||
<DropdownList
|
||||
currentItem={item.column}
|
||||
onItemSelect={handleColumnSelect}
|
||||
items={columns}
|
||||
placeholder="Select a column"
|
||||
/>
|
||||
<DropdownList<ComparisonOperators>
|
||||
<DropdownList
|
||||
currentItem={item.comparisonOperator}
|
||||
onItemSelect={handleSelectComparisonOperator}
|
||||
items={Object.values(ComparisonOperators)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const RowsFilterTableList = ({
|
||||
Item={createRowsFilterComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList<LogicalOperator>
|
||||
<DropdownList
|
||||
currentItem={filter?.logicalOperator}
|
||||
onItemSelect={handleLogicalOperatorChange}
|
||||
items={Object.values(LogicalOperator)}
|
||||
|
||||
@@ -3,11 +3,8 @@ import { ExternalLinkIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { MakeComBlock, Webhook, WebhookOptions } from 'models'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { byId, env } from 'utils'
|
||||
import { byId } from 'utils'
|
||||
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
|
||||
const debounceWebhookTimeout = 2000
|
||||
|
||||
type Props = {
|
||||
block: MakeComBlock
|
||||
@@ -22,19 +19,13 @@ export const MakeComSettings = ({
|
||||
const webhook = webhooks.find(byId(webhookId))
|
||||
|
||||
const [localWebhook, _setLocalWebhook] = useState(webhook)
|
||||
const updateWebhookDebounced = useDebouncedCallback(
|
||||
async (newLocalWebhook) => {
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
},
|
||||
env('E2E_TEST') === 'true' ? 0 : debounceWebhookTimeout
|
||||
)
|
||||
|
||||
const setLocalWebhook = useCallback(
|
||||
(newLocalWebhook: Webhook) => {
|
||||
async (newLocalWebhook: Webhook) => {
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
updateWebhookDebounced(newLocalWebhook)
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
},
|
||||
[updateWebhookDebounced]
|
||||
[updateWebhook]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -51,13 +42,6 @@ export const MakeComSettings = ({
|
||||
})
|
||||
}, [webhook, localWebhook, setLocalWebhook])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
updateWebhookDebounced.flush()
|
||||
},
|
||||
[updateWebhookDebounced]
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Alert status={localWebhook?.url ? 'success' : 'info'} rounded="md">
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { TextInput } from '@/components/inputs/TextInput'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const openAITokensPage = 'https://platform.openai.com/account/api-keys'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onNewCredentials: (id: string) => void
|
||||
}
|
||||
|
||||
export const OpenAICredentialsModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onNewCredentials,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { showToast } = useToast()
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
|
||||
const {
|
||||
credentials: {
|
||||
listCredentials: { refetch: refetchCredentials },
|
||||
},
|
||||
} = trpc.useContext()
|
||||
const { mutate } = trpc.credentials.createCredentials.useMutation({
|
||||
onMutate: () => setIsCreating(true),
|
||||
onSettled: () => setIsCreating(false),
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
description: err.message,
|
||||
status: 'error',
|
||||
})
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
refetchCredentials()
|
||||
onNewCredentials(data.credentialsId)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
const createOpenAICredentials = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!workspace) return
|
||||
mutate({
|
||||
credentials: {
|
||||
type: 'openai',
|
||||
workspaceId: workspace.id,
|
||||
name,
|
||||
data: {
|
||||
apiKey,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Add OpenAI account</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<form onSubmit={createOpenAICredentials}>
|
||||
<ModalBody as={Stack} spacing="6">
|
||||
<TextInput
|
||||
isRequired
|
||||
label="Name"
|
||||
onChange={setName}
|
||||
placeholder="My account"
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
<TextInput
|
||||
isRequired
|
||||
type="password"
|
||||
label="API key"
|
||||
helperText={
|
||||
<>
|
||||
You can generate an API key{' '}
|
||||
<TextLink href={openAITokensPage} isExternal>
|
||||
here
|
||||
</TextLink>
|
||||
.
|
||||
</>
|
||||
}
|
||||
onChange={setApiKey}
|
||||
placeholder="sk-..."
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isCreating}
|
||||
isDisabled={apiKey === '' || name === ''}
|
||||
colorScheme="blue"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Icon, IconProps } from '@chakra-ui/react'
|
||||
|
||||
export const OpenAILogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...props}>
|
||||
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />
|
||||
</Icon>
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SetVariableLabel } from '@/components/SetVariableLabel'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import {
|
||||
ChatCompletionOpenAIOptions,
|
||||
CreateImageOpenAIOptions,
|
||||
OpenAIBlock,
|
||||
} from 'models/features/blocks/integrations/openai'
|
||||
|
||||
type Props = {
|
||||
task: OpenAIBlock['options']['task']
|
||||
responseMapping:
|
||||
| ChatCompletionOpenAIOptions['responseMapping']
|
||||
| CreateImageOpenAIOptions['responseMapping']
|
||||
}
|
||||
|
||||
export const OpenAINodeBody = ({ task, responseMapping }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text color={task ? 'currentcolor' : 'gray.500'} noOfLines={1}>
|
||||
{task ?? 'Configure...'}
|
||||
</Text>
|
||||
{typebot &&
|
||||
responseMapping
|
||||
.map((mapping) => mapping.variableId)
|
||||
.map((variableId, idx) =>
|
||||
variableId ? (
|
||||
<SetVariableLabel
|
||||
key={variableId + idx}
|
||||
variables={typebot.variables}
|
||||
variableId={variableId}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Stack, useDisclosure } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { CredentialsDropdown } from '@/features/credentials'
|
||||
import {
|
||||
ChatCompletionOpenAIOptions,
|
||||
CreateImageOpenAIOptions,
|
||||
defaultChatCompletionOptions,
|
||||
OpenAIBlock,
|
||||
openAITasks,
|
||||
} from 'models/features/blocks/integrations/openai'
|
||||
import { OpenAICredentialsModal } from './OpenAICredentialsModal'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { OpenAIChatCompletionSettings } from './createChatCompletion/OpenAIChatCompletionSettings'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
type OpenAITask = (typeof openAITasks)[number]
|
||||
|
||||
type Props = {
|
||||
options: OpenAIBlock['options']
|
||||
onOptionsChange: (options: OpenAIBlock['options']) => void
|
||||
}
|
||||
|
||||
export const OpenAISettings = ({ options, onOptionsChange }: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
|
||||
const updateCredentialsId = (credentialsId: string | undefined) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
credentialsId,
|
||||
})
|
||||
}
|
||||
|
||||
const updateTask = (task: OpenAITask) => {
|
||||
switch (task) {
|
||||
case 'Create chat completion': {
|
||||
onOptionsChange({
|
||||
credentialsId: options?.credentialsId,
|
||||
...defaultChatCompletionOptions(createId),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{workspace && (
|
||||
<CredentialsDropdown
|
||||
type="openai"
|
||||
workspaceId={workspace.id}
|
||||
currentCredentialsId={options?.credentialsId}
|
||||
onCredentialsSelect={updateCredentialsId}
|
||||
onCreateNewClick={onOpen}
|
||||
/>
|
||||
)}
|
||||
<OpenAICredentialsModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onNewCredentials={updateCredentialsId}
|
||||
/>
|
||||
<DropdownList
|
||||
currentItem={options.task}
|
||||
items={openAITasks.slice(0, -1)}
|
||||
onItemSelect={updateTask}
|
||||
placeholder="Select task"
|
||||
/>
|
||||
{options.task && (
|
||||
<OpenAITaskSettings
|
||||
options={options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const OpenAITaskSettings = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: {
|
||||
options: ChatCompletionOpenAIOptions | CreateImageOpenAIOptions
|
||||
onOptionsChange: (options: OpenAIBlock['options']) => void
|
||||
}) => {
|
||||
switch (options.task) {
|
||||
case 'Create chat completion': {
|
||||
return (
|
||||
<OpenAIChatCompletionSettings
|
||||
options={options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'Create image': {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { TableListItemProps } from '@/components/TableList'
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import {
|
||||
chatCompletionMessageRoles,
|
||||
ChatCompletionOpenAIOptions,
|
||||
} from 'models/features/blocks/integrations/openai'
|
||||
|
||||
type Props = TableListItemProps<ChatCompletionOpenAIOptions['messages'][number]>
|
||||
|
||||
export const ChatCompletionMessageItem = ({ item, onItemChange }: Props) => {
|
||||
const changeRole = (role: (typeof chatCompletionMessageRoles)[number]) => {
|
||||
onItemChange({ ...item, role })
|
||||
}
|
||||
|
||||
const changeContent = (content: string) => {
|
||||
onItemChange({ ...item, content })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList
|
||||
currentItem={item.role}
|
||||
items={chatCompletionMessageRoles}
|
||||
onItemSelect={changeRole}
|
||||
placeholder="Select role"
|
||||
/>
|
||||
<TextInput
|
||||
defaultValue={item.content}
|
||||
onChange={changeContent}
|
||||
placeholder="Content"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { TableListItemProps } from '@/components/TableList'
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { Variable } from 'models'
|
||||
import {
|
||||
ChatCompletionOpenAIOptions,
|
||||
chatCompletionResponseValues,
|
||||
} from 'models/features/blocks/integrations/openai'
|
||||
|
||||
type Props = TableListItemProps<
|
||||
ChatCompletionOpenAIOptions['responseMapping'][number]
|
||||
>
|
||||
|
||||
export const ChatCompletionResponseItem = ({ item, onItemChange }: Props) => {
|
||||
const changeValueToExtract = (
|
||||
valueToExtract: (typeof chatCompletionResponseValues)[number]
|
||||
) => {
|
||||
onItemChange({ ...item, valueToExtract })
|
||||
}
|
||||
|
||||
const changeVariableId = (variable: Pick<Variable, 'id'> | undefined) => {
|
||||
onItemChange({ ...item, variableId: variable ? variable.id : undefined })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<DropdownList
|
||||
currentItem={item.valueToExtract ?? 'Message content'}
|
||||
items={chatCompletionResponseValues}
|
||||
onItemSelect={changeValueToExtract}
|
||||
/>
|
||||
<VariableSearchInput
|
||||
onSelectVariable={changeVariableId}
|
||||
initialVariableId={item.variableId}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { TableList } from '@/components/TableList'
|
||||
import {
|
||||
chatCompletionModels,
|
||||
ChatCompletionOpenAIOptions,
|
||||
} from 'models/features/blocks/integrations/openai'
|
||||
import { ChatCompletionMessageItem } from './ChatCompletionMessageItem'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { ChatCompletionResponseItem } from './ChatCompletionResponseItem'
|
||||
|
||||
const apiReferenceUrl =
|
||||
'https://platform.openai.com/docs/api-reference/chat/create'
|
||||
|
||||
type Props = {
|
||||
options: ChatCompletionOpenAIOptions
|
||||
onOptionsChange: (options: ChatCompletionOpenAIOptions) => void
|
||||
}
|
||||
|
||||
export const OpenAIChatCompletionSettings = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const updateModel = (model: (typeof chatCompletionModels)[number]) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
model,
|
||||
})
|
||||
}
|
||||
|
||||
const updateMessages = (messages: typeof options.messages) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
messages,
|
||||
})
|
||||
}
|
||||
|
||||
const updateResponseMapping = (
|
||||
responseMapping: typeof options.responseMapping
|
||||
) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
responseMapping,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4} pt="2">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
Read the{' '}
|
||||
<TextLink href={apiReferenceUrl} isExternal>
|
||||
API reference
|
||||
</TextLink>{' '}
|
||||
to better understand the available options.
|
||||
</Text>
|
||||
<DropdownList
|
||||
currentItem={options.model}
|
||||
items={chatCompletionModels}
|
||||
onItemSelect={updateModel}
|
||||
/>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Messages
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
|
||||
<AccordionPanel>
|
||||
<TableList
|
||||
initialItems={options.messages}
|
||||
Item={ChatCompletionMessageItem}
|
||||
onItemsChange={updateMessages}
|
||||
isOrdered
|
||||
addLabel="Add message"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Save answer
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
|
||||
<AccordionPanel>
|
||||
<TableList
|
||||
initialItems={options.responseMapping}
|
||||
Item={ChatCompletionResponseItem}
|
||||
onItemsChange={updateResponseMapping}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { IntegrationBlockType } from 'models'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
|
||||
const typebotId = createId()
|
||||
|
||||
test('should be configurable', async ({ page }) => {
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
...parseDefaultGroupWithBlock({
|
||||
type: IntegrationBlockType.OPEN_AI,
|
||||
options: {},
|
||||
}),
|
||||
},
|
||||
])
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.getByText('Configure...').click()
|
||||
await page.getByRole('button', { name: 'Select an account' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Connect new' }).click()
|
||||
await expect(page.getByRole('button', { name: 'Create' })).toBeDisabled()
|
||||
await page.getByPlaceholder('My account').fill('My account')
|
||||
await page.getByPlaceholder('sk-...').fill('sk-test')
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await page.getByRole('button', { name: 'Select task' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Create chat completion' }).click()
|
||||
await page.getByRole('button', { name: 'Messages' }).click()
|
||||
await page.getByRole('button', { name: 'Select role' }).click()
|
||||
await page.getByRole('menuitem', { name: 'system' }).click()
|
||||
await page.getByPlaceholder('Content').first().fill('You are a helpful bot')
|
||||
await page.getByRole('button', { name: 'Add message' }).nth(1).click()
|
||||
await page.getByRole('button', { name: 'Select role' }).click()
|
||||
await page.getByRole('menuitem', { name: 'assistant' }).click()
|
||||
await page.getByPlaceholder('Content').nth(1).fill('Hi there!')
|
||||
await page.getByRole('button', { name: 'Save answer' }).click()
|
||||
await page.getByTestId('variables-input').click()
|
||||
})
|
||||
@@ -2,14 +2,11 @@ import { Alert, AlertIcon, Button, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { PabblyConnectBlock, Webhook, WebhookOptions } from 'models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { byId, env } from 'utils'
|
||||
import React, { useState } from 'react'
|
||||
import { byId } from 'utils'
|
||||
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
|
||||
const debounceWebhookTimeout = 2000
|
||||
|
||||
type Props = {
|
||||
block: PabblyConnectBlock
|
||||
onOptionsChange: (options: WebhookOptions) => void
|
||||
@@ -25,25 +22,11 @@ export const PabblyConnectSettings = ({
|
||||
webhooks.find(byId(webhookId))
|
||||
)
|
||||
|
||||
const updateWebhookDebounced = useDebouncedCallback(
|
||||
async (newLocalWebhook) => {
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
},
|
||||
env('E2E_TEST') === 'true' ? 0 : debounceWebhookTimeout
|
||||
)
|
||||
|
||||
const setLocalWebhook = (newLocalWebhook: Webhook) => {
|
||||
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
updateWebhookDebounced(newLocalWebhook)
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
updateWebhookDebounced.flush()
|
||||
},
|
||||
[updateWebhookDebounced]
|
||||
)
|
||||
|
||||
const handleUrlChange = (url: string) =>
|
||||
localWebhook &&
|
||||
setLocalWebhook({
|
||||
|
||||
@@ -8,14 +8,15 @@ import {
|
||||
FormLabel,
|
||||
} from '@chakra-ui/react'
|
||||
import { CodeEditor } from '@/components/inputs/CodeEditor'
|
||||
import { CredentialsType, SendEmailOptions, Variable } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { SendEmailOptions, Variable } from 'models'
|
||||
import React from 'react'
|
||||
import { env, isNotEmpty } from 'utils'
|
||||
import { SmtpConfigModal } from './SmtpConfigModal'
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { CredentialsDropdown } from '@/features/credentials'
|
||||
import { TextInput, Textarea } from '@/components/inputs'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
|
||||
type Props = {
|
||||
options: SendEmailOptions
|
||||
@@ -23,11 +24,10 @@ type Props = {
|
||||
}
|
||||
|
||||
export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [refreshCredentialsKey, setRefreshCredentialsKey] = useState(0)
|
||||
|
||||
const handleCredentialsSelect = (credentialsId?: string) => {
|
||||
setRefreshCredentialsKey(refreshCredentialsKey + 1)
|
||||
onOptionsChange({
|
||||
...options,
|
||||
credentialsId: credentialsId === undefined ? 'default' : credentialsId,
|
||||
@@ -109,16 +109,18 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<Text>From: </Text>
|
||||
<CredentialsDropdown
|
||||
type={CredentialsType.SMTP}
|
||||
currentCredentialsId={options.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsSelect}
|
||||
onCreateNewClick={onOpen}
|
||||
defaultCredentialLabel={env('SMTP_FROM')
|
||||
?.match(/<(.*)>/)
|
||||
?.pop()}
|
||||
refreshDropdownKey={refreshCredentialsKey}
|
||||
/>
|
||||
{workspace && (
|
||||
<CredentialsDropdown
|
||||
type="smtp"
|
||||
workspaceId={workspace.id}
|
||||
currentCredentialsId={options.credentialsId}
|
||||
onCredentialsSelect={handleCredentialsSelect}
|
||||
onCreateNewClick={onOpen}
|
||||
defaultCredentialLabel={env('SMTP_FROM')
|
||||
?.match(/<(.*)>/)
|
||||
?.pop()}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<TextInput
|
||||
label="Reply to:"
|
||||
|
||||
@@ -2,12 +2,12 @@ import { TextInput, NumberInput } from '@/components/inputs'
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { SmtpCredentialsData } from 'models'
|
||||
import { SmtpCredentials } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
config: SmtpCredentialsData
|
||||
onConfigChange: (config: SmtpCredentialsData) => void
|
||||
config: SmtpCredentials['data']
|
||||
onConfigChange: (config: SmtpCredentials['data']) => void
|
||||
}
|
||||
|
||||
export const SmtpConfigForm = ({ config, onConfigChange }: Props) => {
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
Button,
|
||||
} from '@chakra-ui/react'
|
||||
import { useUser } from '@/features/account'
|
||||
import { CredentialsType, SmtpCredentialsData } from 'models'
|
||||
import React, { useState } from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { SmtpConfigForm } from './SmtpConfigForm'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { testSmtpConfig } from '../../queries/testSmtpConfigQuery'
|
||||
import { createCredentialsQuery } from '@/features/credentials'
|
||||
import { SmtpCredentials } from 'models'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
@@ -33,10 +33,29 @@ export const SmtpConfigModal = ({
|
||||
const { workspace } = useWorkspace()
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const { showToast } = useToast()
|
||||
const [smtpConfig, setSmtpConfig] = useState<SmtpCredentialsData>({
|
||||
const [smtpConfig, setSmtpConfig] = useState<SmtpCredentials['data']>({
|
||||
from: {},
|
||||
port: 25,
|
||||
})
|
||||
const {
|
||||
credentials: {
|
||||
listCredentials: { refetch: refetchCredentials },
|
||||
},
|
||||
} = trpc.useContext()
|
||||
const { mutate } = trpc.credentials.createCredentials.useMutation({
|
||||
onSettled: () => setIsCreating(false),
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
description: err.message,
|
||||
status: 'error',
|
||||
})
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
refetchCredentials()
|
||||
onNewCredentials(data.credentialsId)
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
const handleCreateClick = async () => {
|
||||
if (!user?.email || !workspace?.id) return
|
||||
@@ -53,19 +72,14 @@ export const SmtpConfigModal = ({
|
||||
description: "We couldn't send the test email with your configuration",
|
||||
})
|
||||
}
|
||||
const { data, error } = await createCredentialsQuery({
|
||||
data: smtpConfig,
|
||||
name: smtpConfig.from.email as string,
|
||||
type: CredentialsType.SMTP,
|
||||
workspaceId: workspace.id,
|
||||
mutate({
|
||||
credentials: {
|
||||
data: smtpConfig,
|
||||
name: smtpConfig.from.email as string,
|
||||
type: '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}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SmtpCredentialsData } from 'models'
|
||||
import { SmtpCredentials } from 'models'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const testSmtpConfig = (smtpData: SmtpCredentialsData, to: string) =>
|
||||
export const testSmtpConfig = (smtpData: SmtpCredentials['data'], to: string) =>
|
||||
sendRequest({
|
||||
method: 'POST',
|
||||
url: '/api/integrations/email/test-config',
|
||||
|
||||
@@ -118,7 +118,7 @@ export const WebhookAdvancedConfigForm = ({
|
||||
<>
|
||||
<HStack justify="space-between">
|
||||
<Text>Method:</Text>
|
||||
<DropdownList<HttpMethod>
|
||||
<DropdownList
|
||||
currentItem={webhook.method as HttpMethod}
|
||||
onItemSelect={handleMethodChange}
|
||||
items={Object.values(HttpMethod)}
|
||||
@@ -130,13 +130,12 @@ export const WebhookAdvancedConfigForm = ({
|
||||
Query params
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={4} as={Stack} spacing="6">
|
||||
<AccordionPanel>
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook.queryParams}
|
||||
onItemsChange={handleQueryParamsChange}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
@@ -145,13 +144,12 @@ export const WebhookAdvancedConfigForm = ({
|
||||
Headers
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={4} as={Stack} spacing="6">
|
||||
<AccordionPanel>
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook.headers}
|
||||
onItemsChange={handleHeadersChange}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
@@ -181,7 +179,7 @@ export const WebhookAdvancedConfigForm = ({
|
||||
Variable values for test
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={4} as={Stack} spacing="6">
|
||||
<AccordionPanel>
|
||||
<TableList<VariableForTest>
|
||||
initialItems={
|
||||
options?.variablesForTest ?? { byId: {}, allIds: [] }
|
||||
@@ -189,7 +187,6 @@ export const WebhookAdvancedConfigForm = ({
|
||||
onItemsChange={handleVariablesChange}
|
||||
Item={VariableForTestInputs}
|
||||
addLabel="Add an entry"
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
@@ -215,13 +212,12 @@ export const WebhookAdvancedConfigForm = ({
|
||||
Save in variables
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={4} as={Stack} spacing="6">
|
||||
<AccordionPanel>
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={options.responseVariableMapping}
|
||||
onItemsChange={handleResponseMappingChange}
|
||||
Item={ResponseMappingInputs}
|
||||
addLabel="Add an entry"
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -24,7 +24,6 @@ export const KeyValueInputs = ({
|
||||
onItemChange,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder,
|
||||
debounceTimeout,
|
||||
}: TableListItemProps<KeyValue> & {
|
||||
keyPlaceholder?: string
|
||||
valuePlaceholder?: string
|
||||
@@ -44,14 +43,12 @@ export const KeyValueInputs = ({
|
||||
defaultValue={item.key ?? ''}
|
||||
onChange={handleKeyChange}
|
||||
placeholder={keyPlaceholder}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
<TextInput
|
||||
label="Value:"
|
||||
defaultValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
placeholder={valuePlaceholder}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ import { VariableForTest, Variable } from 'models'
|
||||
export const VariableForTestInputs = ({
|
||||
item,
|
||||
onItemChange,
|
||||
debounceTimeout,
|
||||
}: TableListItemProps<VariableForTest>) => {
|
||||
const handleVariableSelect = (variable?: Variable) =>
|
||||
onItemChange({ ...item, variableId: variable?.id })
|
||||
@@ -29,7 +28,6 @@ export const VariableForTestInputs = ({
|
||||
label="Test value:"
|
||||
defaultValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Spinner, Stack } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { WebhookOptions, Webhook, WebhookBlock } from 'models'
|
||||
import { byId, env } from 'utils'
|
||||
import { byId } from 'utils'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { WebhookAdvancedConfigForm } from '../WebhookAdvancedConfigForm'
|
||||
|
||||
const debounceWebhookTimeout = 2000
|
||||
|
||||
type Props = {
|
||||
block: WebhookBlock
|
||||
onOptionsChange: (options: WebhookOptions) => void
|
||||
@@ -22,26 +19,13 @@ export const WebhookSettings = ({
|
||||
const [localWebhook, _setLocalWebhook] = useState(
|
||||
webhooks.find(byId(webhookId))
|
||||
)
|
||||
const updateWebhookDebounced = useDebouncedCallback(
|
||||
async (newLocalWebhook) => {
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
},
|
||||
env('E2E_TEST') === 'true' ? 0 : debounceWebhookTimeout
|
||||
)
|
||||
|
||||
const setLocalWebhook = (newLocalWebhook: Webhook) => {
|
||||
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
updateWebhookDebounced(newLocalWebhook)
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
updateWebhookDebounced.flush()
|
||||
},
|
||||
[updateWebhookDebounced]
|
||||
)
|
||||
|
||||
const handleUrlChange = (url?: string) =>
|
||||
const updateUrl = (url?: string) =>
|
||||
localWebhook && setLocalWebhook({ ...localWebhook, url: url ?? null })
|
||||
|
||||
if (!localWebhook) return <Spinner />
|
||||
@@ -51,8 +35,7 @@ export const WebhookSettings = ({
|
||||
<TextInput
|
||||
placeholder="Paste webhook URL..."
|
||||
defaultValue={localWebhook.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
debounceTimeout={0}
|
||||
onChange={updateUrl}
|
||||
/>
|
||||
<WebhookAdvancedConfigForm
|
||||
blockId={blockId}
|
||||
|
||||
@@ -3,11 +3,8 @@ import { ExternalLinkIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { Webhook, WebhookOptions, ZapierBlock } from 'models'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { byId, env } from 'utils'
|
||||
import { byId } from 'utils'
|
||||
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
|
||||
const debounceWebhookTimeout = 2000
|
||||
|
||||
type Props = {
|
||||
block: ZapierBlock
|
||||
@@ -23,19 +20,12 @@ export const ZapierSettings = ({
|
||||
|
||||
const [localWebhook, _setLocalWebhook] = useState(webhook)
|
||||
|
||||
const updateWebhookDebounced = useDebouncedCallback(
|
||||
async (newLocalWebhook) => {
|
||||
const setLocalWebhook = useCallback(
|
||||
async (newLocalWebhook: Webhook) => {
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
},
|
||||
env('E2E_TEST') === 'true' ? 0 : debounceWebhookTimeout
|
||||
)
|
||||
|
||||
const setLocalWebhook = useCallback(
|
||||
(newLocalWebhook: Webhook) => {
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
updateWebhookDebounced(newLocalWebhook)
|
||||
},
|
||||
[updateWebhookDebounced]
|
||||
[updateWebhook]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,13 +42,6 @@ export const ZapierSettings = ({
|
||||
})
|
||||
}, [webhook, localWebhook, setLocalWebhook])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
updateWebhookDebounced.flush()
|
||||
},
|
||||
[updateWebhookDebounced]
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Alert status={localWebhook?.url ? 'success' : 'info'} rounded="md">
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ComparisonItem = ({
|
||||
onSelectVariable={handleSelectVariable}
|
||||
placeholder="Search for a variable"
|
||||
/>
|
||||
<DropdownList<ComparisonOperators>
|
||||
<DropdownList
|
||||
currentItem={item.comparisonOperator}
|
||||
onItemSelect={handleSelectComparisonOperator}
|
||||
items={Object.values(ComparisonOperators)}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ConditionItemForm = ({ itemContent, onItemChange }: Props) => {
|
||||
Item={ComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList<LogicalOperator>
|
||||
<DropdownList
|
||||
currentItem={itemContent.logicalOperator}
|
||||
onItemSelect={handleLogicalOperatorChange}
|
||||
items={Object.values(LogicalOperator)}
|
||||
|
||||
Reference in New Issue
Block a user