2
0

feat(integration): Add webhooks

This commit is contained in:
Baptiste Arnaud
2022-01-22 18:24:57 +01:00
parent 66f3e7ee7c
commit a58600a38a
78 changed files with 2399 additions and 800 deletions

View 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} />
)
}

View File

@@ -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

View File

@@ -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()
}

View 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>
)
}

View File

@@ -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([