@ -12,21 +12,23 @@ import {
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
type Props<T> = {
|
||||
currentItem?: T
|
||||
onItemSelect: (item: T) => void
|
||||
items: T[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Props<T extends readonly any[]> = {
|
||||
currentItem: T[number] | undefined
|
||||
onItemSelect: (item: T[number]) => void
|
||||
items: T
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const DropdownList = <T,>({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const DropdownList = <T extends readonly any[]>({
|
||||
currentItem,
|
||||
onItemSelect,
|
||||
items,
|
||||
placeholder = '',
|
||||
...props
|
||||
}: Props<T> & MenuButtonProps) => {
|
||||
const handleMenuItemClick = (operator: T) => () => {
|
||||
const handleMenuItemClick = (operator: T[number]) => () => {
|
||||
onItemSelect(operator)
|
||||
}
|
||||
return (
|
||||
|
27
apps/builder/src/components/SetVariableLabel.tsx
Normal file
27
apps/builder/src/components/SetVariableLabel.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useColorModeValue, HStack, Tag, Text } from '@chakra-ui/react'
|
||||
import { Variable } from 'models'
|
||||
|
||||
export const SetVariableLabel = ({
|
||||
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}>
|
||||
Set
|
||||
</Text>
|
||||
<Tag bg="orange.400" color="white" size="sm">
|
||||
{variableName}
|
||||
</Tag>
|
||||
</HStack>
|
||||
)
|
||||
}
|
@ -1,4 +1,12 @@
|
||||
import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Fade,
|
||||
Flex,
|
||||
IconButton,
|
||||
SlideFade,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { TrashIcon, PlusIcon } from '@/components/icons'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import React, { useState } from 'react'
|
||||
@ -7,26 +15,25 @@ type ItemWithId<T> = T & { id: string }
|
||||
|
||||
export type TableListItemProps<T> = {
|
||||
item: T
|
||||
debounceTimeout?: number
|
||||
onItemChange: (item: T) => void
|
||||
}
|
||||
|
||||
type Props<T> = {
|
||||
initialItems: ItemWithId<T>[]
|
||||
isOrdered?: boolean
|
||||
addLabel?: string
|
||||
debounceTimeout?: number
|
||||
onItemsChange: (items: ItemWithId<T>[]) => void
|
||||
Item: (props: TableListItemProps<T>) => JSX.Element
|
||||
ComponentBetweenItems?: (props: unknown) => JSX.Element
|
||||
onItemsChange: (items: ItemWithId<T>[]) => void
|
||||
}
|
||||
|
||||
export const TableList = <T,>({
|
||||
initialItems,
|
||||
onItemsChange,
|
||||
isOrdered,
|
||||
addLabel = 'Add',
|
||||
debounceTimeout,
|
||||
Item,
|
||||
ComponentBetweenItems,
|
||||
onItemsChange,
|
||||
}: Props<T>) => {
|
||||
const [items, setItems] = useState(initialItems)
|
||||
const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)
|
||||
@ -38,6 +45,15 @@ export const TableList = <T,>({
|
||||
onItemsChange([...items, newItem])
|
||||
}
|
||||
|
||||
const insertItem = (itemIndex: number) => () => {
|
||||
const id = createId()
|
||||
const newItem = { id } as ItemWithId<T>
|
||||
const newItems = [...items]
|
||||
newItems.splice(itemIndex + 1, 0, newItem)
|
||||
setItems(newItems)
|
||||
onItemsChange(newItems)
|
||||
}
|
||||
|
||||
const updateItem = (itemIndex: number, updates: Partial<T>) => {
|
||||
const newItems = items.map((item, idx) =>
|
||||
idx === itemIndex ? { ...item, ...updates } : item
|
||||
@ -62,7 +78,7 @@ export const TableList = <T,>({
|
||||
const handleMouseLeave = () => setShowDeleteIndex(null)
|
||||
|
||||
return (
|
||||
<Stack spacing="4">
|
||||
<Stack spacing={0} pt="2">
|
||||
{items.map((item, itemIndex) => (
|
||||
<Box key={item.id}>
|
||||
{itemIndex !== 0 && ComponentBetweenItems && (
|
||||
@ -73,35 +89,82 @@ export const TableList = <T,>({
|
||||
onMouseEnter={handleMouseEnter(itemIndex)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
mt={itemIndex !== 0 && ComponentBetweenItems ? 4 : 0}
|
||||
justifyContent="center"
|
||||
pb="4"
|
||||
>
|
||||
<Item
|
||||
item={item}
|
||||
onItemChange={handleCellChange(itemIndex)}
|
||||
debounceTimeout={debounceTimeout}
|
||||
/>
|
||||
<Fade in={showDeleteIndex === itemIndex}>
|
||||
<Item item={item} onItemChange={handleCellChange(itemIndex)} />
|
||||
<Fade
|
||||
in={showDeleteIndex === itemIndex}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '-15px',
|
||||
top: '-15px',
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove cell"
|
||||
onClick={deleteItem(itemIndex)}
|
||||
pos="absolute"
|
||||
left="-15px"
|
||||
top="-15px"
|
||||
size="sm"
|
||||
shadow="md"
|
||||
/>
|
||||
</Fade>
|
||||
{isOrdered && (
|
||||
<>
|
||||
{itemIndex === 0 && (
|
||||
<SlideFade
|
||||
offsetY="-5px"
|
||||
in={showDeleteIndex === itemIndex}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-15px',
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<IconButton
|
||||
aria-label={addLabel}
|
||||
icon={<PlusIcon />}
|
||||
size="xs"
|
||||
shadow="md"
|
||||
colorScheme="blue"
|
||||
onClick={insertItem(itemIndex - 1)}
|
||||
/>
|
||||
</SlideFade>
|
||||
)}
|
||||
<SlideFade
|
||||
offsetY="5px"
|
||||
in={showDeleteIndex === itemIndex}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '5px',
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<IconButton
|
||||
aria-label={addLabel}
|
||||
icon={<PlusIcon />}
|
||||
size="xs"
|
||||
shadow="md"
|
||||
colorScheme="blue"
|
||||
onClick={insertItem(itemIndex)}
|
||||
/>
|
||||
</SlideFade>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={createItem}
|
||||
flexShrink={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
{addLabel}
|
||||
</Button>
|
||||
{!isOrdered && (
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={createItem}
|
||||
flexShrink={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
{addLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -16,12 +16,12 @@ import { useDebouncedCallback } from 'use-debounce'
|
||||
import { env } from 'utils'
|
||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||
|
||||
type Value<HasVariable> = HasVariable extends undefined | true
|
||||
type Value<HasVariable> = HasVariable extends true | undefined
|
||||
? number | VariableString
|
||||
: number
|
||||
|
||||
type Props<HasVariable extends boolean> = {
|
||||
defaultValue?: Value<HasVariable>
|
||||
defaultValue: Value<HasVariable> | undefined
|
||||
debounceTimeout?: number
|
||||
withVariableButton?: HasVariable
|
||||
label?: string
|
||||
|
@ -3,6 +3,7 @@ import { injectVariableInText } from '@/features/variables/utils/injectVariableI
|
||||
import { focusInput } from '@/utils/focusInput'
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Input as ChakraInput,
|
||||
@ -26,6 +27,7 @@ export type TextInputProps = {
|
||||
onChange: (value: string) => void
|
||||
debounceTimeout?: number
|
||||
label?: ReactNode
|
||||
helperText?: ReactNode
|
||||
moreInfoTooltip?: string
|
||||
withVariableButton?: boolean
|
||||
isRequired?: boolean
|
||||
@ -42,6 +44,7 @@ export const TextInput = forwardRef(function TextInput(
|
||||
defaultValue,
|
||||
debounceTimeout = 1000,
|
||||
label,
|
||||
helperText,
|
||||
moreInfoTooltip,
|
||||
withVariableButton = true,
|
||||
isRequired,
|
||||
@ -137,6 +140,7 @@ export const TextInput = forwardRef(function TextInput(
|
||||
) : (
|
||||
Input
|
||||
)}
|
||||
{helperText && <FormHelperText>{helperText}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
})
|
||||
|
@ -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)}
|
||||
|
@ -0,0 +1,66 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { stripeCredentialsSchema } from 'models/features/blocks/inputs/payment/schemas'
|
||||
import { googleSheetsCredentialsSchema } from 'models/features/blocks/integrations/googleSheets/schemas'
|
||||
import { openAICredentialsSchema } from 'models/features/blocks/integrations/openai'
|
||||
import { smtpCredentialsSchema } from 'models/features/blocks/integrations/sendEmail'
|
||||
import { encrypt } from 'utils/api/encryption'
|
||||
import { z } from 'zod'
|
||||
|
||||
const inputShape = {
|
||||
data: true,
|
||||
type: true,
|
||||
workspaceId: true,
|
||||
name: true,
|
||||
} as const
|
||||
|
||||
export const createCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/credentials',
|
||||
protect: true,
|
||||
summary: 'Create credentials',
|
||||
tags: ['Credentials'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
credentials: z.discriminatedUnion('type', [
|
||||
stripeCredentialsSchema.pick(inputShape),
|
||||
smtpCredentialsSchema.pick(inputShape),
|
||||
googleSheetsCredentialsSchema.pick(inputShape),
|
||||
openAICredentialsSchema.pick(inputShape),
|
||||
]),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { credentials }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: credentials.workspaceId,
|
||||
members: { some: { userId: user.id } },
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
if (!workspace)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||
|
||||
const { encryptedData, iv } = encrypt(credentials.data)
|
||||
const createdCredentials = await prisma.credentials.create({
|
||||
data: {
|
||||
...credentials,
|
||||
data: encryptedData,
|
||||
iv,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
return { credentialsId: createdCredentials.id }
|
||||
})
|
@ -0,0 +1,49 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const deleteCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/credentials/:credentialsId',
|
||||
protect: true,
|
||||
summary: 'Delete credentials',
|
||||
tags: ['Credentials'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentialsId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(
|
||||
async ({ input: { credentialsId, workspaceId }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
members: { some: { userId: user.id } },
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
if (!workspace)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Workspace not found',
|
||||
})
|
||||
|
||||
await prisma.credentials.delete({
|
||||
where: {
|
||||
id: credentialsId,
|
||||
},
|
||||
})
|
||||
return { credentialsId }
|
||||
}
|
||||
)
|
55
apps/builder/src/features/credentials/api/listCredentials.ts
Normal file
55
apps/builder/src/features/credentials/api/listCredentials.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/utils/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { stripeCredentialsSchema } from 'models/features/blocks/inputs/payment/schemas'
|
||||
import { googleSheetsCredentialsSchema } from 'models/features/blocks/integrations/googleSheets/schemas'
|
||||
import { openAICredentialsSchema } from 'models/features/blocks/integrations/openai'
|
||||
import { smtpCredentialsSchema } from 'models/features/blocks/integrations/sendEmail'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const listCredentials = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/credentials',
|
||||
protect: true,
|
||||
summary: 'List workspace credentials',
|
||||
tags: ['Credentials'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
type: stripeCredentialsSchema.shape.type
|
||||
.or(smtpCredentialsSchema.shape.type)
|
||||
.or(googleSheetsCredentialsSchema.shape.type)
|
||||
.or(openAICredentialsSchema.shape.type),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
credentials: z.array(z.object({ id: z.string(), name: z.string() })),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { workspaceId, type }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findFirst({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
members: { some: { userId: user.id } },
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
if (!workspace)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||
const credentials = await prisma.credentials.findMany({
|
||||
where: {
|
||||
type,
|
||||
workspaceId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
})
|
||||
return { credentials }
|
||||
})
|
10
apps/builder/src/features/credentials/api/router.ts
Normal file
10
apps/builder/src/features/credentials/api/router.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { router } from '@/utils/server/trpc'
|
||||
import { createCredentials } from './createCredentials'
|
||||
import { deleteCredentials } from './deleteCredentials'
|
||||
import { listCredentials } from './listCredentials'
|
||||
|
||||
export const credentialsRouter = router({
|
||||
createCredentials,
|
||||
listCredentials,
|
||||
deleteCredentials,
|
||||
})
|
@ -10,58 +10,73 @@ import {
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon, PlusIcon, TrashIcon } from '@/components/icons'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { CredentialsType } from 'models'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { useToast } from '../../../hooks/useToast'
|
||||
import { deleteCredentialsQuery, useCredentials } from '@/features/credentials'
|
||||
import { Credentials } from 'models'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||
type: CredentialsType
|
||||
type: Credentials['type']
|
||||
workspaceId: string
|
||||
currentCredentialsId?: string
|
||||
onCredentialsSelect: (credentialId?: string) => void
|
||||
onCreateNewClick: () => void
|
||||
defaultCredentialLabel?: string
|
||||
refreshDropdownKey?: number
|
||||
}
|
||||
|
||||
export const CredentialsDropdown = ({
|
||||
type,
|
||||
workspaceId,
|
||||
currentCredentialsId,
|
||||
onCredentialsSelect,
|
||||
onCreateNewClick,
|
||||
defaultCredentialLabel,
|
||||
refreshDropdownKey,
|
||||
...props
|
||||
}: Props) => {
|
||||
const router = useRouter()
|
||||
const { workspace } = useWorkspace()
|
||||
const { showToast } = useToast()
|
||||
const { credentials, mutate } = useCredentials({
|
||||
workspaceId: workspace?.id,
|
||||
const { data, refetch } = trpc.credentials.listCredentials.useQuery({
|
||||
workspaceId,
|
||||
type,
|
||||
})
|
||||
const [isDeleting, setIsDeleting] = useState<string>()
|
||||
const { mutate } = trpc.credentials.deleteCredentials.useMutation({
|
||||
onMutate: ({ credentialsId }) => {
|
||||
setIsDeleting(credentialsId)
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast({
|
||||
description: error.message,
|
||||
})
|
||||
},
|
||||
onSuccess: ({ credentialsId }) => {
|
||||
if (credentialsId === currentCredentialsId) onCredentialsSelect(undefined)
|
||||
refetch()
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsDeleting(undefined)
|
||||
},
|
||||
})
|
||||
|
||||
const defaultCredentialsLabel = defaultCredentialLabel ?? `Select an account`
|
||||
|
||||
const credentialsList = useMemo(() => {
|
||||
return credentials.filter((credential) => credential.type === type)
|
||||
}, [type, credentials])
|
||||
|
||||
const currentCredential = useMemo(
|
||||
() => credentials.find((c) => c.id === currentCredentialsId),
|
||||
[currentCredentialsId, credentials]
|
||||
const currentCredential = data?.credentials.find(
|
||||
(c) => c.id === currentCredentialsId
|
||||
)
|
||||
|
||||
const handleMenuItemClick = (credentialsId: string) => () => {
|
||||
onCredentialsSelect(credentialsId)
|
||||
}
|
||||
const handleMenuItemClick = useCallback(
|
||||
(credentialsId: string) => () => {
|
||||
onCredentialsSelect(credentialsId)
|
||||
},
|
||||
[onCredentialsSelect]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if ((refreshDropdownKey ?? 0) > 0) mutate({ credentials })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refreshDropdownKey])
|
||||
const clearQueryParams = useCallback(() => {
|
||||
const hasQueryParams = router.asPath.includes('?')
|
||||
if (hasQueryParams)
|
||||
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
@ -69,29 +84,17 @@ export const CredentialsDropdown = ({
|
||||
handleMenuItemClick(router.query.credentialsId.toString())()
|
||||
clearQueryParams()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady])
|
||||
}, [
|
||||
clearQueryParams,
|
||||
handleMenuItemClick,
|
||||
router.isReady,
|
||||
router.query.credentialsId,
|
||||
])
|
||||
|
||||
const clearQueryParams = () => {
|
||||
const hasQueryParams = router.asPath.includes('?')
|
||||
if (hasQueryParams)
|
||||
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
|
||||
}
|
||||
|
||||
const handleDeleteDomainClick =
|
||||
const deleteCredentials =
|
||||
(credentialsId: string) => async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!workspace?.id) return
|
||||
setIsDeleting(credentialsId)
|
||||
const { error } = await deleteCredentialsQuery(
|
||||
workspace.id,
|
||||
credentialsId
|
||||
)
|
||||
setIsDeleting(undefined)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
onCredentialsSelect(undefined)
|
||||
mutate({ credentials: credentials.filter((c) => c.id !== credentialsId) })
|
||||
mutate({ workspaceId, credentialsId })
|
||||
}
|
||||
|
||||
return (
|
||||
@ -121,7 +124,7 @@ export const CredentialsDropdown = ({
|
||||
{defaultCredentialLabel}
|
||||
</MenuItem>
|
||||
)}
|
||||
{credentialsList.map((credentials) => (
|
||||
{data?.credentials.map((credentials) => (
|
||||
<MenuItem
|
||||
role="menuitem"
|
||||
minH="40px"
|
||||
@ -137,7 +140,7 @@ export const CredentialsDropdown = ({
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove credentials"
|
||||
size="xs"
|
||||
onClick={handleDeleteDomainClick(credentials.id)}
|
||||
onClick={deleteCredentials(credentials.id)}
|
||||
isLoading={isDeleting === credentials.id}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { fetcher } from '@/utils/helpers'
|
||||
import { Credentials } from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import useSWR from 'swr'
|
||||
|
||||
export const useCredentials = ({
|
||||
workspaceId,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId?: string
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ credentials: Credentials[] }, Error>(
|
||||
workspaceId ? `/api/credentials?${stringify({ workspaceId })}` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error && onError) onError(error)
|
||||
return {
|
||||
credentials: data?.credentials ?? [],
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
@ -1,4 +1 @@
|
||||
export { CredentialsDropdown } from './components/CredentialsDropdown'
|
||||
export { useCredentials } from './hooks/useCredentials'
|
||||
export { createCredentialsQuery } from './queries/createCredentialsQuery'
|
||||
export { deleteCredentialsQuery } from './queries/deleteCredentialsQuery'
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { Credentials } from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const createCredentialsQuery = async (
|
||||
credentials: Omit<Credentials, 'id' | 'iv' | 'createdAt'>
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/credentials?${stringify({
|
||||
workspaceId: credentials.workspaceId,
|
||||
})}`,
|
||||
method: 'POST',
|
||||
body: credentials,
|
||||
})
|
@ -1,14 +0,0 @@
|
||||
import { Credentials } from 'db'
|
||||
import { stringify } from 'qs'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const deleteCredentialsQuery = async (
|
||||
workspaceId: string,
|
||||
credentialsId: string
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/credentials/${credentialsId}?${stringify({ workspaceId })}`,
|
||||
method: 'DELETE',
|
||||
})
|
@ -38,6 +38,7 @@ import { AudioBubbleIcon } from '@/features/blocks/bubbles/audio'
|
||||
import { WaitIcon } from '@/features/blocks/logic/wait/components/WaitIcon'
|
||||
import { ScriptIcon } from '@/features/blocks/logic/script/components/ScriptIcon'
|
||||
import { JumpIcon } from '@/features/blocks/logic/jump/components/JumpIcon'
|
||||
import { OpenAILogo } from '@/features/blocks/integrations/openai/components/OpenAILogo'
|
||||
|
||||
type BlockIconProps = { type: BlockType } & IconProps
|
||||
|
||||
@ -106,6 +107,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
|
||||
return <SendEmailIcon {...props} />
|
||||
case IntegrationBlockType.CHATWOOT:
|
||||
return <ChatwootLogo {...props} />
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return <OpenAILogo {...props} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
}
|
||||
|
@ -73,5 +73,7 @@ export const BlockLabel = ({ type }: Props): JSX.Element => {
|
||||
return <Text>Email</Text>
|
||||
case IntegrationBlockType.CHATWOOT:
|
||||
return <Text>Chatwoot</Text>
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return <Text>OpenAI</Text>
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNod
|
||||
import { ScriptNodeContent } from '@/features/blocks/logic/script/components/ScriptNodeContent'
|
||||
import { ButtonsBlockNode } from '@/features/blocks/inputs/buttons/components/ButtonsBlockNode'
|
||||
import { JumpNodeBody } from '@/features/blocks/logic/jump/components/JumpNodeBody'
|
||||
import { OpenAINodeBody } from '@/features/blocks/integrations/openai/components/OpenAINodeBody'
|
||||
|
||||
type Props = {
|
||||
block: Block | StartBlock
|
||||
@ -169,6 +170,18 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
case IntegrationBlockType.CHATWOOT: {
|
||||
return <ChatwootBlockNodeLabel block={block} />
|
||||
}
|
||||
case IntegrationBlockType.OPEN_AI: {
|
||||
return (
|
||||
<OpenAINodeBody
|
||||
task={block.options.task}
|
||||
responseMapping={
|
||||
'responseMapping' in block.options
|
||||
? block.options.responseMapping
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'start': {
|
||||
return <Text>Start</Text>
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ import { ScriptSettings } from '@/features/blocks/logic/script/components/Script
|
||||
import { JumpSettings } from '@/features/blocks/logic/jump/components/JumpSettings'
|
||||
import { MakeComSettings } from '@/features/blocks/integrations/makeCom/components/MakeComSettings'
|
||||
import { PabblyConnectSettings } from '@/features/blocks/integrations/pabbly/components/PabblyConnectSettings'
|
||||
import { OpenAISettings } from '@/features/blocks/integrations/openai/components/OpenAISettings'
|
||||
|
||||
type Props = {
|
||||
block: BlockWithOptions
|
||||
@ -97,7 +98,7 @@ export const BlockSettings = ({
|
||||
block: BlockWithOptions
|
||||
onBlockChange: (block: Partial<Block>) => void
|
||||
}): JSX.Element => {
|
||||
const handleOptionsChange = (options: BlockOptions) => {
|
||||
const updateOptions = (options: BlockOptions) => {
|
||||
onBlockChange({ options } as Partial<Block>)
|
||||
}
|
||||
|
||||
@ -106,7 +107,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<TextInputSettingsBody
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -114,7 +115,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<NumberInputSettingsBody
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -122,7 +123,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<EmailInputSettingsBody
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -130,7 +131,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<UrlInputSettingsBody
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -138,7 +139,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<DateInputSettingsBody
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -146,7 +147,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<PhoneNumberSettingsBody
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -154,7 +155,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<ButtonsBlockSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -162,7 +163,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<PaymentSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -170,7 +171,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<RatingInputSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -178,7 +179,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<FileInputSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -186,7 +187,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<SetVariableSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -194,7 +195,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<RedirectSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -202,7 +203,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<ScriptSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -210,16 +211,13 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<TypebotLinkForm
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicBlockType.WAIT: {
|
||||
return (
|
||||
<WaitSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
<WaitSettings options={block.options} onOptionsChange={updateOptions} />
|
||||
)
|
||||
}
|
||||
case LogicBlockType.JUMP: {
|
||||
@ -227,7 +225,7 @@ export const BlockSettings = ({
|
||||
<JumpSettings
|
||||
groupId={block.groupId}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -235,7 +233,7 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<GoogleSheetsSettingsBody
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
blockId={block.id}
|
||||
/>
|
||||
)
|
||||
@ -244,38 +242,29 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<GoogleAnalyticsSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.ZAPIER: {
|
||||
return (
|
||||
<ZapierSettings block={block} onOptionsChange={handleOptionsChange} />
|
||||
)
|
||||
return <ZapierSettings block={block} onOptionsChange={updateOptions} />
|
||||
}
|
||||
case IntegrationBlockType.MAKE_COM: {
|
||||
return (
|
||||
<MakeComSettings block={block} onOptionsChange={handleOptionsChange} />
|
||||
)
|
||||
return <MakeComSettings block={block} onOptionsChange={updateOptions} />
|
||||
}
|
||||
case IntegrationBlockType.PABBLY_CONNECT: {
|
||||
return (
|
||||
<PabblyConnectSettings
|
||||
block={block}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
<PabblyConnectSettings block={block} onOptionsChange={updateOptions} />
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.WEBHOOK: {
|
||||
return (
|
||||
<WebhookSettings block={block} onOptionsChange={handleOptionsChange} />
|
||||
)
|
||||
return <WebhookSettings block={block} onOptionsChange={updateOptions} />
|
||||
}
|
||||
case IntegrationBlockType.EMAIL: {
|
||||
return (
|
||||
<SendEmailSettings
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -283,7 +272,15 @@ export const BlockSettings = ({
|
||||
return (
|
||||
<ChatwootSettingsForm
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.OPEN_AI: {
|
||||
return (
|
||||
<OpenAISettings
|
||||
options={block.options}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -426,6 +426,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
|
||||
return defaultSendEmailOptions
|
||||
case IntegrationBlockType.CHATWOOT:
|
||||
return defaultChatwootOptions
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ export const StandardSettings = ({
|
||||
w="70px"
|
||||
value={inputValues.widthValue}
|
||||
/>
|
||||
<DropdownList<string>
|
||||
<DropdownList
|
||||
items={['px', '%']}
|
||||
onItemSelect={handleWidthTypeSelect}
|
||||
currentItem={inputValues.widthType}
|
||||
@ -92,7 +92,7 @@ export const StandardSettings = ({
|
||||
w="70px"
|
||||
value={inputValues.heightValue}
|
||||
/>
|
||||
<DropdownList<string>
|
||||
<DropdownList
|
||||
items={['px', '%']}
|
||||
onItemSelect={handleHeightTypeSelect}
|
||||
currentItem={inputValues.heightType}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Credentials as CredentialsFromDb } from 'db'
|
||||
import { OAuth2Client, Credentials } from 'google-auth-library'
|
||||
import { GoogleSheetsCredentialsData } from 'models'
|
||||
import { GoogleSheetsCredentials } from 'models'
|
||||
import { isDefined } from 'utils'
|
||||
import { decrypt, encrypt } from 'utils/api'
|
||||
import prisma from './prisma'
|
||||
@ -24,7 +24,7 @@ export const getAuthenticatedGoogleClient = async (
|
||||
const data = decrypt(
|
||||
credentials.data,
|
||||
credentials.iv
|
||||
) as GoogleSheetsCredentialsData
|
||||
) as GoogleSheetsCredentials['data']
|
||||
|
||||
oauth2Client.setCredentials(data)
|
||||
oauth2Client.on('tokens', updateTokens(credentials.id, data))
|
||||
@ -32,14 +32,17 @@ export const getAuthenticatedGoogleClient = async (
|
||||
}
|
||||
|
||||
const updateTokens =
|
||||
(credentialsId: string, existingCredentials: GoogleSheetsCredentialsData) =>
|
||||
(
|
||||
credentialsId: string,
|
||||
existingCredentials: GoogleSheetsCredentials['data']
|
||||
) =>
|
||||
async (credentials: Credentials) => {
|
||||
if (
|
||||
isDefined(existingCredentials.id_token) &&
|
||||
credentials.id_token !== existingCredentials.id_token
|
||||
)
|
||||
return
|
||||
const newCredentials: GoogleSheetsCredentialsData = {
|
||||
const newCredentials: GoogleSheetsCredentials['data'] = {
|
||||
...existingCredentials,
|
||||
expiry_date: credentials.expiry_date,
|
||||
access_token: credentials.access_token,
|
||||
|
@ -3,7 +3,6 @@ import { Prisma } from 'db'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { googleSheetsScopes } from './consent-url'
|
||||
import { stringify } from 'querystring'
|
||||
import { CredentialsType } from 'models'
|
||||
import { badRequest, encrypt, notAuthenticated } from 'utils/api'
|
||||
import { oauth2Client } from '@/lib/googleSheets'
|
||||
import { getAuthenticatedUser } from '@/features/auth/api'
|
||||
@ -41,11 +40,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { encryptedData, iv } = encrypt(tokens)
|
||||
const credentials = {
|
||||
name: email,
|
||||
type: CredentialsType.GOOGLE_SHEETS,
|
||||
type: 'google sheets',
|
||||
workspaceId,
|
||||
data: encryptedData,
|
||||
iv,
|
||||
} as Prisma.CredentialsUncheckedCreateInput
|
||||
} satisfies Prisma.CredentialsUncheckedCreateInput
|
||||
const { id: credentialsId } = await prisma.credentials.create({
|
||||
data: credentials,
|
||||
})
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { SmtpCredentialsData } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { createTransport } from 'nodemailer'
|
||||
import { getAuthenticatedUser } from '@/features/auth/api'
|
||||
import { notAuthenticated } from 'utils/api'
|
||||
import { SmtpCredentials } from 'models'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req)
|
||||
@ -10,7 +10,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'POST') {
|
||||
const { from, port, isTlsEnabled, username, password, host, to } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as SmtpCredentialsData & { to: string }
|
||||
) as SmtpCredentials['data'] & { to: string }
|
||||
const transporter = createTransport({
|
||||
host,
|
||||
port,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { billingRouter } from '@/features/billing/api/router'
|
||||
import { webhookRouter } from '@/features/blocks/integrations/webhook/api'
|
||||
import { credentialsRouter } from '@/features/credentials/api/router'
|
||||
import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionProcedure'
|
||||
import { resultsRouter } from '@/features/results/api'
|
||||
import { typebotRouter } from '@/features/typebot/api'
|
||||
@ -13,6 +14,7 @@ export const trpcRouter = router({
|
||||
webhook: webhookRouter,
|
||||
results: resultsRouter,
|
||||
billing: billingRouter,
|
||||
credentials: credentialsRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof trpcRouter
|
||||
|
9
apps/docs/docs/editor/blocks/integrations/openai.md
Normal file
9
apps/docs/docs/editor/blocks/integrations/openai.md
Normal file
@ -0,0 +1,9 @@
|
||||
# OpenAI
|
||||
|
||||
With the OpenAI block, you can create a chat completion based on your user queries and display the answer back to your typebot.
|
||||
|
||||
<img
|
||||
src="/img/blocks/integrations/openai.png"
|
||||
width="600"
|
||||
alt="OpenAI block"
|
||||
/>
|
@ -1979,6 +1979,436 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/credentials": {
|
||||
"post": {
|
||||
"operationId": "mutation.credentials.createCredentials",
|
||||
"summary": "Create credentials",
|
||||
"tags": [
|
||||
"Credentials"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"Authorization": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"credentials": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"live": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicKey": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"secretKey",
|
||||
"publicKey"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"test": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secretKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicKey": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"live",
|
||||
"test"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stripe"
|
||||
]
|
||||
},
|
||||
"workspaceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"type",
|
||||
"workspaceId",
|
||||
"name"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"isTlsEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"port": {
|
||||
"type": "number"
|
||||
},
|
||||
"from": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"port",
|
||||
"from"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"smtp"
|
||||
]
|
||||
},
|
||||
"workspaceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"type",
|
||||
"workspaceId",
|
||||
"name"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refresh_token": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"expiry_date": {
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
},
|
||||
"access_token": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"token_type": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"id_token": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"google sheets"
|
||||
]
|
||||
},
|
||||
"workspaceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"type",
|
||||
"workspaceId",
|
||||
"name"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"apiKey"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"openai"
|
||||
]
|
||||
},
|
||||
"workspaceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"type",
|
||||
"workspaceId",
|
||||
"name"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"credentials"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"credentialsId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"credentialsId"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"operationId": "query.credentials.listCredentials",
|
||||
"summary": "List workspace credentials",
|
||||
"tags": [
|
||||
"Credentials"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"Authorization": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workspaceId",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stripe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"smtp"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"google sheets"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"openai"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"credentials": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"credentials"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/credentials/:credentialsId": {
|
||||
"delete": {
|
||||
"operationId": "mutation.credentials.deleteCredentials",
|
||||
"summary": "Delete credentials",
|
||||
"tags": [
|
||||
"Credentials"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"Authorization": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "credentialsId",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workspaceId",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"credentialsId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"credentialsId"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"$ref": "#/components/responses/error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
File diff suppressed because it is too large
Load Diff
BIN
apps/docs/static/img/blocks/integrations/openai.png
vendored
Normal file
BIN
apps/docs/static/img/blocks/integrations/openai.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
@ -26,6 +26,7 @@
|
||||
"next": "13.1.6",
|
||||
"nextjs-cors": "^2.1.2",
|
||||
"nodemailer": "6.9.1",
|
||||
"openai": "^3.2.1",
|
||||
"phone": "^3.1.34",
|
||||
"qs": "6.11.0",
|
||||
"react": "18.2.0",
|
||||
@ -34,9 +35,9 @@
|
||||
"trpc-openapi": "1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@paralleldrive/cuid2": "2.2.0",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@faire/mjml-react": "3.1.1",
|
||||
"@paralleldrive/cuid2": "2.2.0",
|
||||
"@playwright/test": "1.31.1",
|
||||
"@types/cors": "2.8.13",
|
||||
"@types/google-spreadsheet": "3.3.1",
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
PaymentInputOptions,
|
||||
PaymentInputRuntimeOptions,
|
||||
SessionState,
|
||||
StripeCredentialsData,
|
||||
StripeCredentials,
|
||||
} from 'models'
|
||||
import Stripe from 'stripe'
|
||||
import { decrypt } from 'utils/api/encryption'
|
||||
@ -82,12 +82,12 @@ const createStripePaymentIntent =
|
||||
|
||||
const getStripeInfo = async (
|
||||
credentialsId: string
|
||||
): Promise<StripeCredentialsData | undefined> => {
|
||||
): Promise<StripeCredentials['data'] | undefined> => {
|
||||
const credentials = await prisma.credentials.findUnique({
|
||||
where: { id: credentialsId },
|
||||
})
|
||||
if (!credentials) return
|
||||
return decrypt(credentials.data, credentials.iv) as StripeCredentialsData
|
||||
return decrypt(credentials.data, credentials.iv) as StripeCredentials['data']
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/currencies#zero-decimal
|
||||
|
@ -0,0 +1,85 @@
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { parseVariables, updateVariables } from '@/features/variables/utils'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { SessionState, VariableWithUnknowValue } from 'models'
|
||||
import {
|
||||
ChatCompletionOpenAIOptions,
|
||||
OpenAICredentials,
|
||||
} from 'models/features/blocks/integrations/openai'
|
||||
import { OpenAIApi, Configuration, ChatCompletionRequestMessage } from 'openai'
|
||||
import { isDefined, byId, isNotEmpty } from 'utils'
|
||||
import { decrypt } from 'utils/api/encryption'
|
||||
|
||||
export const createChatCompletionOpenAI = async (
|
||||
state: SessionState,
|
||||
{
|
||||
outgoingEdgeId,
|
||||
options,
|
||||
}: { outgoingEdgeId?: string; options: ChatCompletionOpenAIOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const {
|
||||
typebot: { variables },
|
||||
} = state
|
||||
if (!options.credentialsId) return { outgoingEdgeId }
|
||||
const credentials = await prisma.credentials.findUnique({
|
||||
where: {
|
||||
id: options.credentialsId,
|
||||
},
|
||||
})
|
||||
if (!credentials) return { outgoingEdgeId }
|
||||
const { apiKey } = decrypt(
|
||||
credentials.data,
|
||||
credentials.iv
|
||||
) as OpenAICredentials['data']
|
||||
const configuration = new Configuration({
|
||||
apiKey,
|
||||
})
|
||||
const openai = new OpenAIApi(configuration)
|
||||
const {
|
||||
data: { choices, usage },
|
||||
} = await openai.createChatCompletion({
|
||||
model: options.model,
|
||||
messages: options.messages
|
||||
.map((message) => ({
|
||||
role: message.role,
|
||||
content: parseVariables(variables)(message.content),
|
||||
}))
|
||||
.filter(
|
||||
(message) => isNotEmpty(message.role) && isNotEmpty(message.content)
|
||||
) as ChatCompletionRequestMessage[],
|
||||
})
|
||||
const messageContent = choices[0].message?.content
|
||||
const totalTokens = usage?.total_tokens
|
||||
if (!messageContent) {
|
||||
return { outgoingEdgeId }
|
||||
}
|
||||
const newVariables = options.responseMapping.reduce<
|
||||
VariableWithUnknowValue[]
|
||||
>((newVariables, mapping) => {
|
||||
const existingVariable = variables.find(byId(mapping.variableId))
|
||||
if (!existingVariable) return newVariables
|
||||
if (mapping.valueToExtract === 'Message content') {
|
||||
newVariables.push({
|
||||
...existingVariable,
|
||||
value: messageContent,
|
||||
})
|
||||
}
|
||||
if (mapping.valueToExtract === 'Total tokens' && isDefined(totalTokens)) {
|
||||
newVariables.push({
|
||||
...existingVariable,
|
||||
value: totalTokens,
|
||||
})
|
||||
}
|
||||
return newVariables
|
||||
}, [])
|
||||
if (newVariables.length > 0) {
|
||||
const newSessionState = await updateVariables(state)(newVariables)
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
newSessionState,
|
||||
}
|
||||
}
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { SessionState } from 'models'
|
||||
import { OpenAIBlock } from 'models/features/blocks/integrations/openai'
|
||||
import { createChatCompletionOpenAI } from './createChatCompletionOpenAI'
|
||||
|
||||
export const executeOpenAIBlock = async (
|
||||
state: SessionState,
|
||||
block: OpenAIBlock
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
switch (block.options.task) {
|
||||
case 'Create chat completion':
|
||||
return createChatCompletionOpenAI(state, {
|
||||
options: block.options,
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
})
|
||||
case 'Create image':
|
||||
case undefined:
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import {
|
||||
SendEmailBlock,
|
||||
SendEmailOptions,
|
||||
SessionState,
|
||||
SmtpCredentialsData,
|
||||
SmtpCredentials,
|
||||
} from 'models'
|
||||
import { createTransport } from 'nodemailer'
|
||||
import Mail from 'nodemailer/lib/mailer'
|
||||
@ -161,7 +161,7 @@ const sendEmail = async ({
|
||||
|
||||
const getEmailInfo = async (
|
||||
credentialsId: string
|
||||
): Promise<SmtpCredentialsData | undefined> => {
|
||||
): Promise<SmtpCredentials['data'] | undefined> => {
|
||||
if (credentialsId === 'default')
|
||||
return {
|
||||
host: defaultTransportOptions.host,
|
||||
@ -175,7 +175,7 @@ const getEmailInfo = async (
|
||||
where: { id: credentialsId },
|
||||
})
|
||||
if (!credentials) return
|
||||
return decrypt(credentials.data, credentials.iv) as SmtpCredentialsData
|
||||
return decrypt(credentials.data, credentials.iv) as SmtpCredentials['data']
|
||||
}
|
||||
|
||||
const getEmailBody = async ({
|
||||
|
@ -1,11 +1,11 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { createSmtpCredentials } from '../../../../test/utils/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { SmtpCredentialsData } from 'models'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
import { SmtpCredentials } from 'models'
|
||||
|
||||
export const mockSmtpCredentials: SmtpCredentialsData = {
|
||||
export const mockSmtpCredentials: SmtpCredentials['data'] = {
|
||||
from: {
|
||||
email: 'sunny.cremin66@ethereal.email',
|
||||
name: 'Sunny Cremin',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { executeChatwootBlock } from '@/features/blocks/integrations/chatwoot/api'
|
||||
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/api'
|
||||
import { executeGoogleSheetBlock } from '@/features/blocks/integrations/googleSheets/api'
|
||||
import { executeOpenAIBlock } from '@/features/blocks/integrations/openai/executeOpenAIBlock'
|
||||
import { executeSendEmailBlock } from '@/features/blocks/integrations/sendEmail/api'
|
||||
import { executeWebhookBlock } from '@/features/blocks/integrations/webhook/api'
|
||||
import { IntegrationBlock, IntegrationBlockType, SessionState } from 'models'
|
||||
@ -23,5 +24,7 @@ export const executeIntegration =
|
||||
case IntegrationBlockType.MAKE_COM:
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
return executeWebhookBlock(state, block)
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return executeOpenAIBlock(state, block)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Credentials as CredentialsFromDb } from 'db'
|
||||
import { OAuth2Client, Credentials } from 'google-auth-library'
|
||||
import { GoogleSheetsCredentialsData } from 'models'
|
||||
import { GoogleSheetsCredentials } from 'models'
|
||||
import { isDefined } from 'utils'
|
||||
import { decrypt, encrypt } from 'utils/api'
|
||||
import prisma from './prisma'
|
||||
@ -15,7 +15,7 @@ export const getAuthenticatedGoogleClient = async (
|
||||
const data = decrypt(
|
||||
credentials.data,
|
||||
credentials.iv
|
||||
) as GoogleSheetsCredentialsData
|
||||
) as GoogleSheetsCredentials['data']
|
||||
|
||||
const oauth2Client = new OAuth2Client(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
@ -28,14 +28,17 @@ export const getAuthenticatedGoogleClient = async (
|
||||
}
|
||||
|
||||
const updateTokens =
|
||||
(credentialsId: string, existingCredentials: GoogleSheetsCredentialsData) =>
|
||||
(
|
||||
credentialsId: string,
|
||||
existingCredentials: GoogleSheetsCredentials['data']
|
||||
) =>
|
||||
async (credentials: Credentials) => {
|
||||
if (
|
||||
isDefined(existingCredentials.id_token) &&
|
||||
credentials.id_token !== existingCredentials.id_token
|
||||
)
|
||||
return
|
||||
const newCredentials: GoogleSheetsCredentialsData = {
|
||||
const newCredentials: GoogleSheetsCredentials['data'] = {
|
||||
...existingCredentials,
|
||||
expiry_date: credentials.expiry_date,
|
||||
access_token: credentials.access_token,
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
import Stripe from 'stripe'
|
||||
|
||||
import Cors from 'cors'
|
||||
import { PaymentInputOptions, StripeCredentialsData, Variable } from 'models'
|
||||
import { PaymentInputOptions, StripeCredentials, Variable } from 'models'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
|
||||
@ -103,12 +103,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
const getStripeInfo = async (
|
||||
credentialsId: string
|
||||
): Promise<StripeCredentialsData | undefined> => {
|
||||
): Promise<StripeCredentials['data'] | undefined> => {
|
||||
const credentials = await prisma.credentials.findUnique({
|
||||
where: { id: credentialsId },
|
||||
})
|
||||
if (!credentials) return
|
||||
return decrypt(credentials.data, credentials.iv) as StripeCredentialsData
|
||||
return decrypt(credentials.data, credentials.iv) as StripeCredentials['data']
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/currencies#zero-decimal
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
PublicTypebot,
|
||||
ResultValues,
|
||||
SendEmailOptions,
|
||||
SmtpCredentialsData,
|
||||
SmtpCredentials,
|
||||
} from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { createTransport, getTestMessageUrl } from 'nodemailer'
|
||||
@ -155,7 +155,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
const getEmailInfo = async (
|
||||
credentialsId: string
|
||||
): Promise<SmtpCredentialsData | undefined> => {
|
||||
): Promise<SmtpCredentials['data'] | undefined> => {
|
||||
if (credentialsId === 'default')
|
||||
return {
|
||||
host: defaultTransportOptions.host,
|
||||
@ -169,7 +169,7 @@ const getEmailInfo = async (
|
||||
where: { id: credentialsId },
|
||||
})
|
||||
if (!credentials) return
|
||||
return decrypt(credentials.data, credentials.iv) as SmtpCredentialsData
|
||||
return decrypt(credentials.data, credentials.iv) as SmtpCredentials['data']
|
||||
}
|
||||
|
||||
const getEmailBody = async ({
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CredentialsType, SmtpCredentialsData } from 'models'
|
||||
import { PrismaClient } from 'db'
|
||||
import { SmtpCredentials } from 'models'
|
||||
import { encrypt } from 'utils/api'
|
||||
import { proWorkspaceId } from 'utils/playwright/databaseSetup'
|
||||
|
||||
@ -7,7 +7,7 @@ const prisma = new PrismaClient()
|
||||
|
||||
export const createSmtpCredentials = (
|
||||
id: string,
|
||||
smtpData: SmtpCredentialsData
|
||||
smtpData: SmtpCredentials['data']
|
||||
) => {
|
||||
const { encryptedData, iv } = encrypt(smtpData)
|
||||
return prisma.credentials.create({
|
||||
@ -16,7 +16,7 @@ export const createSmtpCredentials = (
|
||||
data: encryptedData,
|
||||
iv,
|
||||
name: smtpData.from.email as string,
|
||||
type: CredentialsType.SMTP,
|
||||
type: 'smtp',
|
||||
workspaceId: proWorkspaceId,
|
||||
},
|
||||
})
|
||||
|
@ -1,35 +1,29 @@
|
||||
import { z } from 'zod'
|
||||
import { schemaForType } from './utils'
|
||||
import { Answer as AnswerPrisma, Prisma } from 'db'
|
||||
|
||||
export const answerSchema = schemaForType<AnswerPrisma>()(
|
||||
z.object({
|
||||
createdAt: z.date(),
|
||||
resultId: z.string(),
|
||||
blockId: z.string(),
|
||||
groupId: z.string(),
|
||||
variableId: z.string().nullable(),
|
||||
content: z.string(),
|
||||
storageUsed: z.number().nullable(),
|
||||
})
|
||||
)
|
||||
export const answerSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
resultId: z.string(),
|
||||
blockId: z.string(),
|
||||
groupId: z.string(),
|
||||
variableId: z.string().nullable(),
|
||||
content: z.string(),
|
||||
storageUsed: z.number().nullable(),
|
||||
}) satisfies z.ZodType<AnswerPrisma>
|
||||
|
||||
export const answerInputSchema =
|
||||
schemaForType<Prisma.AnswerUncheckedUpdateInput>()(
|
||||
answerSchema
|
||||
.omit({
|
||||
createdAt: true,
|
||||
resultId: true,
|
||||
variableId: true,
|
||||
storageUsed: true,
|
||||
})
|
||||
.and(
|
||||
z.object({
|
||||
variableId: z.string().nullish(),
|
||||
storageUsed: z.number().nullish(),
|
||||
})
|
||||
)
|
||||
)
|
||||
export const answerInputSchema = answerSchema
|
||||
.omit({
|
||||
createdAt: true,
|
||||
resultId: true,
|
||||
variableId: true,
|
||||
storageUsed: true,
|
||||
})
|
||||
.and(
|
||||
z.object({
|
||||
variableId: z.string().nullish(),
|
||||
storageUsed: z.number().nullish(),
|
||||
})
|
||||
) satisfies z.ZodType<Prisma.AnswerUncheckedUpdateInput>
|
||||
|
||||
export type Stats = {
|
||||
totalViews: number
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
import { Credentials as CredentialsFromPrisma } from 'db'
|
||||
|
||||
export const blockBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
@ -9,3 +10,11 @@ export const blockBaseSchema = z.object({
|
||||
export const optionBaseSchema = z.object({
|
||||
variableId: z.string().optional(),
|
||||
})
|
||||
|
||||
export const credentialsBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
workspaceId: z.string(),
|
||||
name: z.string(),
|
||||
iv: z.string(),
|
||||
}) satisfies z.ZodType<Omit<CredentialsFromPrisma, 'data' | 'type'>>
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { z } from 'zod'
|
||||
import { optionBaseSchema, blockBaseSchema } from '../../baseSchemas'
|
||||
import {
|
||||
optionBaseSchema,
|
||||
blockBaseSchema,
|
||||
credentialsBaseSchema,
|
||||
} from '../../baseSchemas'
|
||||
import { InputBlockType } from '../enums'
|
||||
import { PaymentProvider } from './enums'
|
||||
|
||||
@ -43,6 +47,22 @@ export const paymentInputSchema = blockBaseSchema.and(
|
||||
})
|
||||
)
|
||||
|
||||
export const stripeCredentialsSchema = z
|
||||
.object({
|
||||
type: z.literal('stripe'),
|
||||
data: z.object({
|
||||
live: z.object({
|
||||
secretKey: z.string(),
|
||||
publicKey: z.string(),
|
||||
}),
|
||||
test: z.object({
|
||||
secretKey: z.string().optional(),
|
||||
publicKey: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.merge(credentialsBaseSchema)
|
||||
|
||||
export const defaultPaymentInputOptions: PaymentInputOptions = {
|
||||
provider: PaymentProvider.STRIPE,
|
||||
labels: { button: 'Pay', success: 'Success' },
|
||||
@ -54,3 +74,4 @@ export type PaymentInputOptions = z.infer<typeof paymentInputOptionsSchema>
|
||||
export type PaymentInputRuntimeOptions = z.infer<
|
||||
typeof paymentInputRuntimeOptionsSchema
|
||||
>
|
||||
export type StripeCredentials = z.infer<typeof stripeCredentialsSchema>
|
||||
|
@ -1,5 +1,6 @@
|
||||
export enum IntegrationBlockType {
|
||||
GOOGLE_SHEETS = 'Google Sheets',
|
||||
OPEN_AI = 'OpenAI',
|
||||
GOOGLE_ANALYTICS = 'Google Analytics',
|
||||
WEBHOOK = 'Webhook',
|
||||
EMAIL = 'Email',
|
||||
|
@ -2,7 +2,7 @@ import { z } from 'zod'
|
||||
import { ComparisonOperators, LogicalOperator } from '../../logic/condition'
|
||||
import { IntegrationBlockType } from '../enums'
|
||||
import { GoogleSheetsAction } from './enums'
|
||||
import { blockBaseSchema } from '../../baseSchemas'
|
||||
import { blockBaseSchema, credentialsBaseSchema } from '../../baseSchemas'
|
||||
|
||||
const cellSchema = z.object({
|
||||
column: z.string().optional(),
|
||||
@ -69,6 +69,20 @@ export const googleSheetsBlockSchema = blockBaseSchema.and(
|
||||
})
|
||||
)
|
||||
|
||||
export const googleSheetsCredentialsSchema = z
|
||||
.object({
|
||||
type: z.literal('google sheets'),
|
||||
data: z.object({
|
||||
refresh_token: z.string().nullish(),
|
||||
expiry_date: z.number().nullish(),
|
||||
access_token: z.string().nullish(),
|
||||
token_type: z.string().nullish(),
|
||||
id_token: z.string().nullish(),
|
||||
scope: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
.merge(credentialsBaseSchema)
|
||||
|
||||
export const defaultGoogleSheetsOptions: GoogleSheetsOptions = {}
|
||||
|
||||
export const defaultGoogleSheetsGetOptions = (
|
||||
@ -129,3 +143,6 @@ export type GoogleSheetsUpdateRowOptions = z.infer<
|
||||
export type Cell = z.infer<typeof cellSchema>
|
||||
export type ExtractingCell = z.infer<typeof extractingCellSchema>
|
||||
export type RowsFilterComparison = z.infer<typeof rowsFilterComparisonSchema>
|
||||
export type GoogleSheetsCredentials = z.infer<
|
||||
typeof googleSheetsCredentialsSchema
|
||||
>
|
||||
|
114
packages/models/features/blocks/integrations/openai.ts
Normal file
114
packages/models/features/blocks/integrations/openai.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { z } from 'zod'
|
||||
import { blockBaseSchema, credentialsBaseSchema } from '../baseSchemas'
|
||||
import { IntegrationBlockType } from './enums'
|
||||
|
||||
export const openAITasks = ['Create chat completion', 'Create image'] as const
|
||||
|
||||
export const chatCompletionModels = [
|
||||
'gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-0301',
|
||||
] as const
|
||||
|
||||
export const chatCompletionMessageRoles = [
|
||||
'system',
|
||||
'user',
|
||||
'assistant',
|
||||
] as const
|
||||
|
||||
export const chatCompletionResponseValues = [
|
||||
'Message content',
|
||||
'Total tokens',
|
||||
] as const
|
||||
|
||||
const openAIBaseOptionsSchema = z.object({
|
||||
credentialsId: z.string().optional(),
|
||||
})
|
||||
|
||||
const initialOptionsSchema = z
|
||||
.object({
|
||||
task: z.undefined(),
|
||||
})
|
||||
.merge(openAIBaseOptionsSchema)
|
||||
|
||||
const chatCompletionMessageSchema = z.object({
|
||||
id: z.string(),
|
||||
role: z.enum(chatCompletionMessageRoles).optional(),
|
||||
content: z.string().optional(),
|
||||
})
|
||||
|
||||
const chatCompletionOptionsSchema = z
|
||||
.object({
|
||||
task: z.literal(openAITasks[0]),
|
||||
model: z.enum(chatCompletionModels),
|
||||
messages: z.array(chatCompletionMessageSchema),
|
||||
responseMapping: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
valueToExtract: z.enum(chatCompletionResponseValues),
|
||||
variableId: z.string().optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
.merge(openAIBaseOptionsSchema)
|
||||
|
||||
const createImageOptionsSchema = z
|
||||
.object({
|
||||
task: z.literal(openAITasks[1]),
|
||||
prompt: z.string().optional(),
|
||||
advancedOptions: z.object({
|
||||
size: z.enum(['256x256', '512x512', '1024x1024']).optional(),
|
||||
}),
|
||||
responseMapping: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
valueToExtract: z.enum(['Image URL']),
|
||||
variableId: z.string().optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
.merge(openAIBaseOptionsSchema)
|
||||
|
||||
export const openAIBlockSchema = blockBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.enum([IntegrationBlockType.OPEN_AI]),
|
||||
options: z.discriminatedUnion('task', [
|
||||
initialOptionsSchema,
|
||||
chatCompletionOptionsSchema,
|
||||
createImageOptionsSchema,
|
||||
]),
|
||||
})
|
||||
)
|
||||
|
||||
export const openAICredentialsSchema = z
|
||||
.object({
|
||||
type: z.literal('openai'),
|
||||
data: z.object({
|
||||
apiKey: z.string(),
|
||||
}),
|
||||
})
|
||||
.merge(credentialsBaseSchema)
|
||||
|
||||
export const defaultChatCompletionOptions = (
|
||||
createId: () => string
|
||||
): ChatCompletionOpenAIOptions => ({
|
||||
task: 'Create chat completion',
|
||||
messages: [
|
||||
{
|
||||
id: createId(),
|
||||
},
|
||||
],
|
||||
responseMapping: [
|
||||
{
|
||||
id: createId(),
|
||||
valueToExtract: 'Message content',
|
||||
},
|
||||
],
|
||||
model: 'gpt-3.5-turbo',
|
||||
})
|
||||
|
||||
export type OpenAICredentials = z.infer<typeof openAICredentialsSchema>
|
||||
export type OpenAIBlock = z.infer<typeof openAIBlockSchema>
|
||||
export type ChatCompletionOpenAIOptions = z.infer<
|
||||
typeof chatCompletionOptionsSchema
|
||||
>
|
||||
export type CreateImageOpenAIOptions = z.infer<typeof createImageOptionsSchema>
|
@ -1,25 +1,14 @@
|
||||
import { z } from 'zod'
|
||||
import { chatwootBlockSchema, chatwootOptionsSchema } from './chatwoot'
|
||||
import {
|
||||
googleAnalyticsOptionsSchema,
|
||||
googleAnalyticsBlockSchema,
|
||||
} from './googleAnalytics'
|
||||
import {
|
||||
googleSheetsOptionsSchema,
|
||||
googleSheetsBlockSchema,
|
||||
} from './googleSheets/schemas'
|
||||
import { chatwootBlockSchema } from './chatwoot'
|
||||
import { googleAnalyticsBlockSchema } from './googleAnalytics'
|
||||
import { googleSheetsBlockSchema } from './googleSheets/schemas'
|
||||
import { makeComBlockSchema } from './makeCom'
|
||||
import { openAIBlockSchema } from './openai'
|
||||
import { pabblyConnectBlockSchema } from './pabblyConnect'
|
||||
import { sendEmailOptionsSchema, sendEmailBlockSchema } from './sendEmail'
|
||||
import { webhookOptionsSchema, webhookBlockSchema } from './webhook'
|
||||
import { sendEmailBlockSchema } from './sendEmail'
|
||||
import { webhookBlockSchema } from './webhook'
|
||||
import { zapierBlockSchema } from './zapier'
|
||||
|
||||
const integrationBlockOptionsSchema = googleSheetsOptionsSchema
|
||||
.or(googleAnalyticsOptionsSchema)
|
||||
.or(webhookOptionsSchema)
|
||||
.or(sendEmailOptionsSchema)
|
||||
.or(chatwootOptionsSchema)
|
||||
|
||||
export const integrationBlockSchema = googleSheetsBlockSchema
|
||||
.or(googleAnalyticsBlockSchema)
|
||||
.or(webhookBlockSchema)
|
||||
@ -28,8 +17,7 @@ export const integrationBlockSchema = googleSheetsBlockSchema
|
||||
.or(makeComBlockSchema)
|
||||
.or(pabblyConnectBlockSchema)
|
||||
.or(chatwootBlockSchema)
|
||||
.or(openAIBlockSchema)
|
||||
|
||||
export type IntegrationBlock = z.infer<typeof integrationBlockSchema>
|
||||
export type IntegrationBlockOptions = z.infer<
|
||||
typeof integrationBlockOptionsSchema
|
||||
>
|
||||
export type IntegrationBlockOptions = IntegrationBlock['options']
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
import { blockBaseSchema } from '../baseSchemas'
|
||||
import { blockBaseSchema, credentialsBaseSchema } from '../baseSchemas'
|
||||
import { IntegrationBlockType } from './enums'
|
||||
|
||||
export const sendEmailOptionsSchema = z.object({
|
||||
@ -22,6 +22,23 @@ export const sendEmailBlockSchema = blockBaseSchema.and(
|
||||
})
|
||||
)
|
||||
|
||||
export const smtpCredentialsSchema = z
|
||||
.object({
|
||||
type: z.literal('smtp'),
|
||||
data: z.object({
|
||||
host: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
isTlsEnabled: z.boolean().optional(),
|
||||
port: z.number(),
|
||||
from: z.object({
|
||||
email: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.merge(credentialsBaseSchema)
|
||||
|
||||
export const defaultSendEmailOptions: SendEmailOptions = {
|
||||
credentialsId: 'default',
|
||||
isCustomBody: false,
|
||||
@ -30,3 +47,4 @@ export const defaultSendEmailOptions: SendEmailOptions = {
|
||||
|
||||
export type SendEmailBlock = z.infer<typeof sendEmailBlockSchema>
|
||||
export type SendEmailOptions = z.infer<typeof sendEmailOptionsSchema>
|
||||
export type SmtpCredentials = z.infer<typeof smtpCredentialsSchema>
|
||||
|
@ -1,58 +1,14 @@
|
||||
import { Credentials as CredentialsFromPrisma } from 'db'
|
||||
import { z } from 'zod'
|
||||
import { stripeCredentialsSchema } from './blocks/inputs/payment/schemas'
|
||||
import { googleSheetsCredentialsSchema } from './blocks/integrations/googleSheets/schemas'
|
||||
import { openAICredentialsSchema } from './blocks/integrations/openai'
|
||||
import { smtpCredentialsSchema } from './blocks/integrations/sendEmail'
|
||||
|
||||
export type Credentials =
|
||||
| SmtpCredentials
|
||||
| GoogleSheetsCredentials
|
||||
| StripeCredentials
|
||||
export const credentialsSchema = z.discriminatedUnion('type', [
|
||||
smtpCredentialsSchema,
|
||||
googleSheetsCredentialsSchema,
|
||||
stripeCredentialsSchema,
|
||||
openAICredentialsSchema,
|
||||
])
|
||||
|
||||
export type CredentialsBase = Omit<CredentialsFromPrisma, 'data' | 'type'>
|
||||
|
||||
export enum CredentialsType {
|
||||
GOOGLE_SHEETS = 'google sheets',
|
||||
SMTP = 'smtp',
|
||||
STRIPE = 'stripe',
|
||||
}
|
||||
|
||||
export type SmtpCredentials = CredentialsBase & {
|
||||
type: CredentialsType.SMTP
|
||||
data: SmtpCredentialsData
|
||||
}
|
||||
|
||||
export type GoogleSheetsCredentials = CredentialsBase & {
|
||||
type: CredentialsType.GOOGLE_SHEETS
|
||||
data: GoogleSheetsCredentialsData
|
||||
}
|
||||
|
||||
export type StripeCredentials = CredentialsBase & {
|
||||
type: CredentialsType.STRIPE
|
||||
data: StripeCredentialsData
|
||||
}
|
||||
|
||||
export type GoogleSheetsCredentialsData = {
|
||||
refresh_token?: string | null
|
||||
expiry_date?: number | null
|
||||
access_token?: string | null
|
||||
token_type?: string | null
|
||||
id_token?: string | null
|
||||
scope?: string
|
||||
}
|
||||
|
||||
export type SmtpCredentialsData = {
|
||||
host?: string
|
||||
username?: string
|
||||
password?: string
|
||||
isTlsEnabled?: boolean
|
||||
port: number
|
||||
from: { email?: string; name?: string }
|
||||
}
|
||||
|
||||
export type StripeCredentialsData = {
|
||||
live: {
|
||||
secretKey: string
|
||||
publicKey: string
|
||||
}
|
||||
test?: {
|
||||
secretKey?: string
|
||||
publicKey?: string
|
||||
}
|
||||
}
|
||||
export type Credentials = z.infer<typeof credentialsSchema>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { PublicTypebot as PrismaPublicTypebot } from 'db'
|
||||
import {
|
||||
groupSchema,
|
||||
edgeSchema,
|
||||
@ -6,24 +7,20 @@ import {
|
||||
settingsSchema,
|
||||
typebotSchema,
|
||||
} from './typebot'
|
||||
import { PublicTypebot as PublicTypebotPrisma } from 'db'
|
||||
import { z } from 'zod'
|
||||
import { schemaForType } from './utils'
|
||||
|
||||
export const publicTypebotSchema = schemaForType<PublicTypebotPrisma>()(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
version: z.enum(['3']).nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
typebotId: z.string(),
|
||||
groups: z.array(groupSchema),
|
||||
edges: z.array(edgeSchema),
|
||||
variables: z.array(variableSchema),
|
||||
theme: themeSchema,
|
||||
settings: settingsSchema,
|
||||
})
|
||||
)
|
||||
export const publicTypebotSchema = z.object({
|
||||
id: z.string(),
|
||||
version: z.enum(['3']).nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
typebotId: z.string(),
|
||||
groups: z.array(groupSchema),
|
||||
edges: z.array(edgeSchema),
|
||||
variables: z.array(variableSchema),
|
||||
theme: themeSchema,
|
||||
settings: settingsSchema,
|
||||
}) satisfies z.ZodType<PrismaPublicTypebot>
|
||||
|
||||
const publicTypebotWithName = publicTypebotSchema.and(
|
||||
typebotSchema.pick({ name: true, isArchived: true, isClosed: true })
|
||||
|
@ -3,19 +3,16 @@ import { answerInputSchema, answerSchema } from './answer'
|
||||
import { InputBlockType } from './blocks'
|
||||
import { variableWithValueSchema } from './typebot/variable'
|
||||
import { Result as ResultPrisma, Log as LogPrisma } from 'db'
|
||||
import { schemaForType } from './utils'
|
||||
|
||||
export const resultSchema = schemaForType<ResultPrisma>()(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
typebotId: z.string(),
|
||||
variables: z.array(variableWithValueSchema),
|
||||
isCompleted: z.boolean(),
|
||||
hasStarted: z.boolean().nullable(),
|
||||
isArchived: z.boolean().nullable(),
|
||||
})
|
||||
)
|
||||
export const resultSchema = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
typebotId: z.string(),
|
||||
variables: z.array(variableWithValueSchema),
|
||||
isCompleted: z.boolean(),
|
||||
hasStarted: z.boolean().nullable(),
|
||||
isArchived: z.boolean().nullable(),
|
||||
}) satisfies z.ZodType<ResultPrisma>
|
||||
|
||||
export const resultWithAnswersSchema = resultSchema.and(
|
||||
z.object({
|
||||
@ -29,16 +26,14 @@ export const resultWithAnswersInputSchema = resultSchema.and(
|
||||
})
|
||||
)
|
||||
|
||||
export const logSchema = schemaForType<LogPrisma>()(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
resultId: z.string(),
|
||||
status: z.string(),
|
||||
description: z.string(),
|
||||
details: z.string().nullable(),
|
||||
})
|
||||
)
|
||||
export const logSchema = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
resultId: z.string(),
|
||||
status: z.string(),
|
||||
description: z.string(),
|
||||
details: z.string().nullable(),
|
||||
}) satisfies z.ZodType<LogPrisma>
|
||||
|
||||
export type Result = z.infer<typeof resultSchema>
|
||||
export type ResultWithAnswers = z.infer<typeof resultWithAnswersSchema>
|
||||
|
@ -4,7 +4,6 @@ import { blockSchema } from '../blocks'
|
||||
import { themeSchema } from './theme'
|
||||
import { variableSchema } from './variable'
|
||||
import { Typebot as TypebotPrisma } from 'db'
|
||||
import { schemaForType } from '../utils'
|
||||
|
||||
export const groupSchema = z.object({
|
||||
id: z.string(),
|
||||
@ -39,28 +38,26 @@ const resultsTablePreferencesSchema = z.object({
|
||||
columnsWidth: z.record(z.string(), z.number()),
|
||||
})
|
||||
|
||||
export const typebotSchema = schemaForType<TypebotPrisma>()(
|
||||
z.object({
|
||||
version: z.enum(['3']).nullable(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
groups: z.array(groupSchema),
|
||||
edges: z.array(edgeSchema),
|
||||
variables: z.array(variableSchema),
|
||||
theme: themeSchema,
|
||||
settings: settingsSchema,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
icon: z.string().nullable(),
|
||||
folderId: z.string().nullable(),
|
||||
publicId: z.string().nullable(),
|
||||
customDomain: z.string().nullable(),
|
||||
workspaceId: z.string(),
|
||||
resultsTablePreferences: resultsTablePreferencesSchema.nullable(),
|
||||
isArchived: z.boolean(),
|
||||
isClosed: z.boolean(),
|
||||
})
|
||||
)
|
||||
export const typebotSchema = z.object({
|
||||
version: z.enum(['3']).nullable(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
groups: z.array(groupSchema),
|
||||
edges: z.array(edgeSchema),
|
||||
variables: z.array(variableSchema),
|
||||
theme: themeSchema,
|
||||
settings: settingsSchema,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
icon: z.string().nullable(),
|
||||
folderId: z.string().nullable(),
|
||||
publicId: z.string().nullable(),
|
||||
customDomain: z.string().nullable(),
|
||||
workspaceId: z.string(),
|
||||
resultsTablePreferences: resultsTablePreferencesSchema.nullable(),
|
||||
isArchived: z.boolean(),
|
||||
isClosed: z.boolean(),
|
||||
}) satisfies z.ZodType<TypebotPrisma>
|
||||
|
||||
export type Typebot = z.infer<typeof typebotSchema>
|
||||
export type Target = z.infer<typeof targetSchema>
|
||||
|
@ -2,12 +2,6 @@ import { z } from 'zod'
|
||||
|
||||
export type IdMap<T> = { [id: string]: T }
|
||||
|
||||
export const schemaForType =
|
||||
<T>() =>
|
||||
<S extends z.ZodType<T, any, any>>(arg: S) => {
|
||||
return arg
|
||||
}
|
||||
|
||||
export const variableStringSchema = z.custom<`{{${string}}}`>((val) =>
|
||||
/^{{.+}}$/g.test(val as string)
|
||||
)
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { z } from 'zod'
|
||||
import { schemaForType } from './utils'
|
||||
import {
|
||||
Workspace as WorkspacePrisma,
|
||||
Plan,
|
||||
@ -9,53 +8,47 @@ import {
|
||||
WorkspaceInvitation as WorkspaceInvitationPrisma,
|
||||
} from 'db'
|
||||
|
||||
export const workspaceMemberSchema = schemaForType<
|
||||
export const workspaceMemberSchema = z.object({
|
||||
workspaceId: z.string(),
|
||||
user: z.object({
|
||||
name: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
image: z.string().nullable(),
|
||||
}),
|
||||
role: z.nativeEnum(WorkspaceRole),
|
||||
}) satisfies z.ZodType<
|
||||
Omit<MemberInWorkspacePrisma, 'userId' | 'createdAt' | 'updatedAt'> & {
|
||||
user: Pick<UserPrisma, 'name' | 'email' | 'image'>
|
||||
}
|
||||
>()(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
user: z.object({
|
||||
name: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
image: z.string().nullable(),
|
||||
}),
|
||||
role: z.nativeEnum(WorkspaceRole),
|
||||
})
|
||||
)
|
||||
>
|
||||
|
||||
export const workspaceInvitationSchema = schemaForType<
|
||||
export const workspaceInvitationSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
email: z.string(),
|
||||
type: z.nativeEnum(WorkspaceRole),
|
||||
}) satisfies z.ZodType<
|
||||
Omit<WorkspaceInvitationPrisma, 'workspaceId' | 'userId' | 'id'>
|
||||
>()(
|
||||
z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
email: z.string(),
|
||||
type: z.nativeEnum(WorkspaceRole),
|
||||
})
|
||||
)
|
||||
>
|
||||
|
||||
export const workspaceSchema = schemaForType<WorkspacePrisma>()(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
name: z.string(),
|
||||
icon: z.string().nullable(),
|
||||
plan: z.nativeEnum(Plan),
|
||||
stripeId: z.string().nullable(),
|
||||
additionalChatsIndex: z.number(),
|
||||
additionalStorageIndex: z.number(),
|
||||
chatsLimitFirstEmailSentAt: z.date().nullable(),
|
||||
chatsLimitSecondEmailSentAt: z.date().nullable(),
|
||||
storageLimitFirstEmailSentAt: z.date().nullable(),
|
||||
storageLimitSecondEmailSentAt: z.date().nullable(),
|
||||
customChatsLimit: z.number().nullable(),
|
||||
customStorageLimit: z.number().nullable(),
|
||||
customSeatsLimit: z.number().nullable(),
|
||||
})
|
||||
)
|
||||
export const workspaceSchema = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
name: z.string(),
|
||||
icon: z.string().nullable(),
|
||||
plan: z.nativeEnum(Plan),
|
||||
stripeId: z.string().nullable(),
|
||||
additionalChatsIndex: z.number(),
|
||||
additionalStorageIndex: z.number(),
|
||||
chatsLimitFirstEmailSentAt: z.date().nullable(),
|
||||
chatsLimitSecondEmailSentAt: z.date().nullable(),
|
||||
storageLimitFirstEmailSentAt: z.date().nullable(),
|
||||
storageLimitSecondEmailSentAt: z.date().nullable(),
|
||||
customChatsLimit: z.number().nullable(),
|
||||
customStorageLimit: z.number().nullable(),
|
||||
customSeatsLimit: z.number().nullable(),
|
||||
}) satisfies z.ZodType<WorkspacePrisma>
|
||||
|
||||
export type Workspace = z.infer<typeof workspaceSchema>
|
||||
export type WorkspaceMember = z.infer<typeof workspaceMemberSchema>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { GraphNavigation, Plan, PrismaClient, WorkspaceRole } from 'db'
|
||||
import { CredentialsType } from 'models'
|
||||
import { encrypt } from '../api'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
@ -134,7 +133,7 @@ const setupCredentials = () => {
|
||||
data: [
|
||||
{
|
||||
name: 'pro-user@email.com',
|
||||
type: CredentialsType.GOOGLE_SHEETS,
|
||||
type: 'google sheets',
|
||||
data: encryptedData,
|
||||
workspaceId: proWorkspaceId,
|
||||
iv,
|
||||
|
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -376,6 +376,7 @@ importers:
|
||||
nextjs-cors: ^2.1.2
|
||||
node-fetch: ^3.3.0
|
||||
nodemailer: 6.9.1
|
||||
openai: ^3.2.1
|
||||
papaparse: 5.3.2
|
||||
phone: ^3.1.34
|
||||
qs: 6.11.0
|
||||
@ -402,6 +403,7 @@ importers:
|
||||
next: 13.1.6_6m24vuloj5ihw4zc5lbsktc4fu
|
||||
nextjs-cors: 2.1.2_next@13.1.6
|
||||
nodemailer: 6.9.1
|
||||
openai: 3.2.1
|
||||
phone: 3.1.34
|
||||
qs: 6.11.0
|
||||
react: 18.2.0
|
||||
@ -15642,6 +15644,15 @@ packages:
|
||||
is-wsl: 2.2.0
|
||||
dev: false
|
||||
|
||||
/openai/3.2.1:
|
||||
resolution: {integrity: sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==}
|
||||
dependencies:
|
||||
axios: 0.26.1
|
||||
form-data: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/openapi-to-postmanv2/1.2.7:
|
||||
resolution: {integrity: sha512-oG3PZfAAljy5ebot8DZGLFDNNmDZ/qWqI/dboWlgg5hRj6dSSrXeiyXL6VQpcGDalxVX4jSChufOq2eDsFXp4w==}
|
||||
engines: {node: '>=4'}
|
||||
|
Reference in New Issue
Block a user