feat(integration): ✨ Add webhooks
This commit is contained in:
58
apps/builder/components/shared/CodeEditor.tsx
Normal file
58
apps/builder/components/shared/CodeEditor.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Box, BoxProps } from '@chakra-ui/react'
|
||||
import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup'
|
||||
import { json } from '@codemirror/lang-json'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange?: (value: string) => void
|
||||
isReadOnly?: boolean
|
||||
}
|
||||
export const CodeEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
isReadOnly = false,
|
||||
...props
|
||||
}: Props & Omit<BoxProps, 'onChange'>) => {
|
||||
const editorContainer = useRef<HTMLDivElement | null>(null)
|
||||
const editorView = useRef<EditorView | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorView.current || !isReadOnly) return
|
||||
editorView.current.dispatch({
|
||||
changes: { from: 0, insert: value },
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorContainer.current) return
|
||||
const updateListenerExtension = EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && onChange)
|
||||
onChange(update.state.doc.toJSON().join(' '))
|
||||
})
|
||||
const editor = new EditorView({
|
||||
state: EditorState.create({
|
||||
extensions: [
|
||||
updateListenerExtension,
|
||||
basicSetup,
|
||||
json(),
|
||||
EditorState.readOnly.of(isReadOnly),
|
||||
],
|
||||
}),
|
||||
parent: editorContainer.current,
|
||||
})
|
||||
editor.dispatch({
|
||||
changes: { from: 0, insert: value },
|
||||
})
|
||||
editorView.current = editor
|
||||
return () => {
|
||||
editor.destroy()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box ref={editorContainer} h="200px" data-testid="code-editor" {...props} />
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Input,
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react'
|
||||
import { UserIcon } from 'assets/icons'
|
||||
import { Variable } from 'models'
|
||||
@@ -29,12 +31,12 @@ export const InputWithVariableButton = ({
|
||||
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
onChange(debouncedValue)
|
||||
if (debouncedValue !== initialValue) onChange(debouncedValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedValue])
|
||||
|
||||
const handleVariableSelected = (variable: Variable) => {
|
||||
if (!inputRef.current) return
|
||||
const handleVariableSelected = (variable?: Variable) => {
|
||||
if (!inputRef.current || !variable) return
|
||||
const cursorPosition = carretPosition
|
||||
const textBeforeCursorPosition = inputRef.current.value.substring(
|
||||
0,
|
||||
@@ -67,7 +69,7 @@ export const InputWithVariableButton = ({
|
||||
setValue(e.target.value)
|
||||
|
||||
return (
|
||||
<HStack>
|
||||
<HStack spacing={0}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onKeyUp={handleKeyUp}
|
||||
@@ -79,12 +81,15 @@ export const InputWithVariableButton = ({
|
||||
/>
|
||||
<Popover matchWidth isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="Insert a variable"
|
||||
icon={<UserIcon />}
|
||||
pos="relative"
|
||||
ml="2"
|
||||
/>
|
||||
<Flex>
|
||||
<Tooltip label="Insert a variable">
|
||||
<IconButton
|
||||
aria-label="Insert a variable"
|
||||
icon={<UserIcon />}
|
||||
pos="relative"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="full">
|
||||
<VariableSearchInput
|
||||
|
||||
@@ -11,20 +11,23 @@ import {
|
||||
InputProps,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, useRef, useEffect, ChangeEvent } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onSelectItem: (value: string) => void
|
||||
onValueChange?: (value: string) => void
|
||||
} & InputProps
|
||||
|
||||
export const SearchableDropdown = ({
|
||||
selectedItem,
|
||||
items,
|
||||
onSelectItem,
|
||||
onValueChange,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
const [inputValue, setInputValue] = useState(selectedItem)
|
||||
const [inputValue, setInputValue] = useState(selectedItem ?? '')
|
||||
const [debouncedInputValue] = useDebounce(inputValue, 200)
|
||||
const [filteredItems, setFilteredItems] = useState([
|
||||
...items
|
||||
.filter((item) =>
|
||||
@@ -52,6 +55,13 @@ export const SearchableDropdown = ({
|
||||
handler: onClose,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onValueChange &&
|
||||
debouncedInputValue !== selectedItem &&
|
||||
onValueChange(debouncedInputValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedInputValue])
|
||||
|
||||
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value)
|
||||
if (e.target.value === '') {
|
||||
@@ -69,7 +79,6 @@ export const SearchableDropdown = ({
|
||||
|
||||
const handleItemClick = (item: string) => () => {
|
||||
setInputValue(item)
|
||||
onSelectItem(item)
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
||||
114
apps/builder/components/shared/TableList.tsx
Normal file
114
apps/builder/components/shared/TableList.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
|
||||
import { TrashIcon, PlusIcon } from 'assets/icons'
|
||||
import { Draft } from 'immer'
|
||||
import { Table } from 'models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { generate } from 'short-uuid'
|
||||
import { useImmer } from 'use-immer'
|
||||
|
||||
export type TableListItemProps<T> = {
|
||||
id: string
|
||||
item: T
|
||||
onItemChange: (item: T) => void
|
||||
}
|
||||
|
||||
type Props<T> = {
|
||||
initialItems?: Table<T>
|
||||
onItemsChange: (items: Table<T>) => void
|
||||
addLabel?: string
|
||||
Item: (props: TableListItemProps<T>) => JSX.Element
|
||||
ComponentBetweenItems?: (props: unknown) => JSX.Element
|
||||
}
|
||||
|
||||
export const TableList = <T,>({
|
||||
initialItems,
|
||||
onItemsChange,
|
||||
addLabel = 'Add',
|
||||
Item,
|
||||
ComponentBetweenItems = () => <></>,
|
||||
}: Props<T>) => {
|
||||
const [items, setItems] = useImmer(initialItems ?? { byId: {}, allIds: [] })
|
||||
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (items.allIds.length === 0) createItem()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
onItemsChange(items)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [items])
|
||||
|
||||
const createItem = () => {
|
||||
setItems((items) => {
|
||||
const id = generate()
|
||||
items.byId[id] = { id } as unknown as Draft<T>
|
||||
items.allIds.push(id)
|
||||
})
|
||||
}
|
||||
|
||||
const updateItem = (itemId: string, updates: Partial<T>) =>
|
||||
setItems((items) => {
|
||||
items.byId[itemId] = {
|
||||
...items.byId[itemId],
|
||||
...updates,
|
||||
}
|
||||
})
|
||||
|
||||
const deleteItem = (itemId: string) => () => {
|
||||
setItems((items) => {
|
||||
delete items.byId[itemId]
|
||||
const index = items.allIds.indexOf(itemId)
|
||||
if (index !== -1) items.allIds.splice(index, 1)
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseEnter = (itemId: string) => () => setShowDeleteId(itemId)
|
||||
|
||||
const handleCellChange = (itemId: string) => (item: T) =>
|
||||
updateItem(itemId, item)
|
||||
|
||||
const handleMouseLeave = () => setShowDeleteId(undefined)
|
||||
|
||||
return (
|
||||
<Stack spacing="4">
|
||||
{items.allIds.map((itemId, idx) => (
|
||||
<Box key={itemId}>
|
||||
{idx !== 0 && <ComponentBetweenItems />}
|
||||
<Flex
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(itemId)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Item
|
||||
id={itemId}
|
||||
item={items.byId[itemId]}
|
||||
onItemChange={handleCellChange(itemId)}
|
||||
/>
|
||||
<Fade in={showDeleteId === itemId}>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove cell"
|
||||
onClick={deleteItem(itemId)}
|
||||
pos="absolute"
|
||||
left="-15px"
|
||||
top="-15px"
|
||||
size="sm"
|
||||
shadow="md"
|
||||
/>
|
||||
</Fade>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={createItem}
|
||||
flexShrink={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
{addLabel}
|
||||
</Button>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -15,11 +15,14 @@ import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { Variable } from 'models'
|
||||
import React, { useState, useRef, ChangeEvent, useMemo, useEffect } from 'react'
|
||||
import { generate } from 'short-uuid'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
initialVariableId?: string
|
||||
onSelectVariable: (variable: Pick<Variable, 'id' | 'name'>) => void
|
||||
onSelectVariable: (
|
||||
variable: Pick<Variable, 'id' | 'name'> | undefined
|
||||
) => void
|
||||
isDefaultOpen?: boolean
|
||||
} & InputProps
|
||||
|
||||
@@ -39,6 +42,7 @@ export const VariableSearchInput = ({
|
||||
const [inputValue, setInputValue] = useState(
|
||||
typebot?.variables.byId[initialVariableId ?? '']?.name ?? ''
|
||||
)
|
||||
const [debouncedInputValue] = useDebounce(inputValue, 200)
|
||||
const [filteredItems, setFilteredItems] = useState<Variable[]>(variables)
|
||||
const dropdownRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
@@ -53,11 +57,18 @@ export const VariableSearchInput = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const variable = variables.find((v) => v.name === debouncedInputValue)
|
||||
if (variable) onSelectVariable(variable)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedInputValue])
|
||||
|
||||
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value)
|
||||
onOpen()
|
||||
if (e.target.value === '') {
|
||||
setFilteredItems([...variables.slice(0, 50)])
|
||||
onSelectVariable(undefined)
|
||||
return
|
||||
}
|
||||
setFilteredItems([
|
||||
|
||||
Reference in New Issue
Block a user