Introducing The Forge (#1072)

The Forge allows anyone to easily create their own Typebot Block.

Closes #380
This commit is contained in:
Baptiste Arnaud
2023-12-13 10:22:02 +01:00
committed by GitHub
parent c373108b55
commit 5e019bbb22
184 changed files with 42659 additions and 37411 deletions

View File

@@ -2,6 +2,10 @@ import {
Button,
ButtonProps,
chakra,
FormControl,
FormHelperText,
FormLabel,
HStack,
Menu,
MenuButton,
MenuItem,
@@ -10,7 +14,8 @@ import {
Stack,
} from '@chakra-ui/react'
import { ChevronLeftIcon } from '@/components/icons'
import React from 'react'
import React, { ReactNode } from 'react'
import { MoreInfoTooltip } from './MoreInfoTooltip'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Props<T extends readonly any[]> = {
@@ -18,6 +23,11 @@ type Props<T extends readonly any[]> = {
onItemSelect: (item: T[number]) => void
items: T
placeholder?: string
label?: string
isRequired?: boolean
direction?: 'row' | 'column'
helperText?: ReactNode
moreInfoTooltip?: string
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -25,44 +35,67 @@ export const DropdownList = <T extends readonly any[]>({
currentItem,
onItemSelect,
items,
placeholder = '',
placeholder,
label,
isRequired,
direction = 'column',
helperText,
moreInfoTooltip,
...props
}: Props<T> & ButtonProps) => {
const handleMenuItemClick = (operator: T[number]) => () => {
onItemSelect(operator)
}
return (
<Menu isLazy placement="bottom-end" matchWidth>
<MenuButton
as={Button}
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
colorScheme="gray"
justifyContent="space-between"
textAlign="left"
{...props}
>
<chakra.span noOfLines={1} display="block">
{currentItem ?? placeholder}
</chakra.span>
</MenuButton>
<Portal>
<MenuList maxW="500px" zIndex={1500}>
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
{items.map((item) => (
<MenuItem
key={item as unknown as string}
maxW="500px"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
onClick={handleMenuItemClick(item)}
>
{item}
</MenuItem>
))}
</Stack>
</MenuList>
</Portal>
</Menu>
<FormControl
isRequired={isRequired}
as={direction === 'column' ? Stack : HStack}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (
<FormLabel display="flex" flexShrink={0} gap="1" mb="0" mr="0">
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
<Menu isLazy placement="bottom-end" matchWidth>
<MenuButton
as={Button}
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
colorScheme="gray"
justifyContent="space-between"
textAlign="left"
w="full"
{...props}
>
<chakra.span noOfLines={1} display="block">
{currentItem ?? placeholder ?? 'Select an item'}
</chakra.span>
</MenuButton>
<Portal>
<MenuList maxW="500px" zIndex={1500}>
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
{items.map((item) => (
<MenuItem
key={item as unknown as string}
maxW="500px"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
onClick={handleMenuItemClick(item)}
>
{item}
</MenuItem>
))}
</Stack>
</MenuList>
</Portal>
</Menu>
{helperText && <FormHelperText>{helperText}</FormHelperText>}
</FormControl>
)
}

View File

@@ -28,9 +28,9 @@ type Props<T> = {
addLabel?: string
newItemDefaultProps?: Partial<T>
hasDefaultItem?: boolean
Item: (props: TableListItemProps<T>) => JSX.Element
ComponentBetweenItems?: (props: unknown) => JSX.Element
onItemsChange: (items: ItemWithId<T>[]) => void
children: (props: TableListItemProps<T>) => JSX.Element
}
export const TableList = <T,>({
@@ -39,7 +39,7 @@ export const TableList = <T,>({
addLabel = 'Add',
newItemDefaultProps,
hasDefaultItem,
Item,
children,
ComponentBetweenItems,
onItemsChange,
}: Props<T>) => {
@@ -107,7 +107,7 @@ export const TableList = <T,>({
justifyContent="center"
pb="4"
>
<Item item={item} onItemChange={handleCellChange(itemIndex)} />
{children({ item, onItemChange: handleCellChange(itemIndex) })}
<Fade
in={showDeleteIndex === itemIndex}
style={{

View File

@@ -11,9 +11,10 @@ import {
FormLabel,
Stack,
Text,
FormHelperText,
} from '@chakra-ui/react'
import { Variable, VariableString } from '@typebot.io/schemas'
import { useEffect, useState } from 'react'
import { ReactNode, useEffect, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
@@ -31,6 +32,7 @@ type Props<HasVariable extends boolean> = {
isRequired?: boolean
direction?: 'row' | 'column'
suffix?: string
helperText?: ReactNode
onValueChange: (value?: Value<HasVariable>) => void
} & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'>
@@ -42,8 +44,9 @@ export const NumberInput = <HasVariable extends boolean>({
label,
moreInfoTooltip,
isRequired,
direction,
direction = 'column',
suffix,
helperText,
...props
}: Props<HasVariable>) => {
const [value, setValue] = useState(defaultValue?.toString() ?? '')
@@ -87,7 +90,12 @@ export const NumberInput = <HasVariable extends boolean>({
}
const Input = (
<ChakraNumberInput onChange={handleValueChange} value={value} {...props}>
<ChakraNumberInput
onChange={handleValueChange}
value={value}
w="full"
{...props}
>
<NumberInputField placeholder={props.placeholder} />
<NumberInputStepper>
<NumberIncrementStepper />
@@ -105,16 +113,16 @@ export const NumberInput = <HasVariable extends boolean>({
spacing={direction === 'column' ? 2 : 3}
>
{label && (
<FormLabel mb="0" mr="0" flexShrink={0}>
<FormLabel display="flex" flexShrink={0} gap="1" mb="0" mr="0">
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
<HStack>
<HStack w={direction === 'row' ? undefined : 'full'}>
{withVariableButton ?? true ? (
<HStack spacing="0">
<HStack spacing="0" w="full">
{Input}
<VariablesButton onSelectVariable={handleVariableSelected} />
</HStack>
@@ -123,6 +131,7 @@ export const NumberInput = <HasVariable extends boolean>({
)}
{suffix ? <Text>{suffix}</Text> : null}
</HStack>
{helperText ? <FormHelperText mt="0">{helperText}</FormHelperText> : null}
</FormControl>
)
}

View File

@@ -160,7 +160,7 @@ export const TextInput = forwardRef(function TextInput(
) : (
Input
)}
{helperText && <FormHelperText>{helperText}</FormHelperText>}
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
</FormControl>
)
})

View File

@@ -7,9 +7,11 @@ import {
HStack,
Textarea as ChakraTextarea,
TextareaProps,
FormHelperText,
Stack,
} from '@chakra-ui/react'
import { Variable } from '@typebot.io/schemas'
import React, { useEffect, useRef, useState } from 'react'
import React, { ReactNode, useEffect, useRef, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
@@ -23,7 +25,9 @@ type Props = {
withVariableButton?: boolean
isRequired?: boolean
placeholder?: string
helperText?: ReactNode
onChange: (value: string) => void
direction?: 'row' | 'column'
} & Pick<TextareaProps, 'minH'>
export const Textarea = ({
@@ -37,6 +41,8 @@ export const Textarea = ({
withVariableButton = true,
isRequired,
minH,
helperText,
direction = 'column',
}: Props) => {
const inputRef = useRef<HTMLTextAreaElement | null>(null)
const [isTouched, setIsTouched] = useState(false)
@@ -93,14 +99,20 @@ export const Textarea = ({
onBlur={updateCarretPosition}
onChange={(e) => changeValue(e.target.value)}
placeholder={placeholder}
minH={minH}
minH={minH ?? '150px'}
/>
)
return (
<FormControl isRequired={isRequired}>
<FormControl
isRequired={isRequired}
as={direction === 'column' ? Stack : HStack}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (
<FormLabel>
<FormLabel display="flex" flexShrink={0} gap="1" mb="0" mr="0">
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
@@ -115,6 +127,7 @@ export const Textarea = ({
) : (
Textarea
)}
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
</FormControl>
)
}

View File

@@ -13,15 +13,26 @@ import {
Portal,
Tag,
Text,
FormControl,
FormLabel,
FormHelperText,
Stack,
} from '@chakra-ui/react'
import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { createId } from '@paralleldrive/cuid2'
import { Variable } from '@typebot.io/schemas'
import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
import React, {
useState,
useRef,
ChangeEvent,
useEffect,
ReactNode,
} from 'react'
import { byId, isDefined, isNotDefined } from '@typebot.io/lib'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
type Props = {
initialVariableId: string | undefined
@@ -29,12 +40,23 @@ type Props = {
onSelectVariable: (
variable: Pick<Variable, 'id' | 'name'> | undefined
) => void
} & InputProps
label?: string
placeholder?: string
helperText?: ReactNode
moreInfoTooltip?: string
direction?: 'row' | 'column'
} & Omit<InputProps, 'placeholder'>
export const VariableSearchInput = ({
initialVariableId,
onSelectVariable,
autoFocus,
placeholder,
label,
helperText,
moreInfoTooltip,
direction = 'column',
isRequired,
...inputProps
}: Props) => {
const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
@@ -168,114 +190,133 @@ export const VariableSearchInput = ({
}
return (
<Flex ref={dropdownRef} w="full">
<Popover
isOpen={isOpen}
initialFocusRef={inputRef}
matchWidth
isLazy
offset={[0, 2]}
>
<PopoverAnchor>
<Input
data-testid="variables-input"
ref={inputRef}
value={inputValue}
onChange={onInputChange}
onFocus={openDropdown}
onKeyDown={handleKeyUp}
placeholder={inputProps.placeholder ?? 'Select a variable'}
autoComplete="off"
{...inputProps}
/>
</PopoverAnchor>
<Portal containerRef={parentModalRef}>
<PopoverContent
maxH="35vh"
overflowY="scroll"
role="menu"
w="inherit"
shadow="lg"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
{isCreateVariableButtonDisplayed && (
<Button
ref={createVariableItemRef}
role="menuitem"
minH="40px"
onClick={handleCreateNewVariableClick}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PlusIcon />}
bgColor={
keyboardFocusIndex === 0 ? focusedItemBgColor : 'transparent'
}
>
Create
<Tag colorScheme="orange" ml="1">
<Text noOfLines={0} display="block">
{inputValue}
</Text>
</Tag>
</Button>
)}
{filteredItems.length > 0 && (
<>
{filteredItems.map((item, idx) => {
const indexInList = isCreateVariableButtonDisplayed
? idx + 1
: idx
return (
<Button
ref={(el) => (itemsRef.current[idx] = el)}
role="menuitem"
minH="40px"
key={idx}
onClick={handleVariableNameClick(item)}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
variant="ghost"
justifyContent="space-between"
bgColor={
keyboardFocusIndex === indexInList
? focusedItemBgColor
: 'transparent'
}
transition="none"
>
<Text noOfLines={0} display="block" pr="2">
{item.name}
</Text>
<FormControl
isRequired={isRequired}
as={direction === 'column' ? Stack : HStack}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (
<FormLabel display="flex" flexShrink={0} gap="1" mb="0" mr="0">
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
<Flex ref={dropdownRef} w="full">
<Popover
isOpen={isOpen}
initialFocusRef={inputRef}
matchWidth
isLazy
offset={[0, 2]}
>
<PopoverAnchor>
<Input
data-testid="variables-input"
ref={inputRef}
value={inputValue}
onChange={onInputChange}
onFocus={openDropdown}
onKeyDown={handleKeyUp}
placeholder={placeholder ?? 'Select a variable'}
autoComplete="off"
{...inputProps}
/>
</PopoverAnchor>
<Portal containerRef={parentModalRef}>
<PopoverContent
maxH="35vh"
overflowY="scroll"
role="menu"
w="inherit"
shadow="lg"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
{isCreateVariableButtonDisplayed && (
<Button
ref={createVariableItemRef}
role="menuitem"
minH="40px"
onClick={handleCreateNewVariableClick}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
variant="ghost"
justifyContent="flex-start"
leftIcon={<PlusIcon />}
bgColor={
keyboardFocusIndex === 0
? focusedItemBgColor
: 'transparent'
}
>
Create
<Tag colorScheme="orange" ml="1">
<Text noOfLines={0} display="block">
{inputValue}
</Text>
</Tag>
</Button>
)}
{filteredItems.length > 0 && (
<>
{filteredItems.map((item, idx) => {
const indexInList = isCreateVariableButtonDisplayed
? idx + 1
: idx
return (
<Button
ref={(el) => (itemsRef.current[idx] = el)}
role="menuitem"
minH="40px"
key={idx}
onClick={handleVariableNameClick(item)}
fontSize="16px"
fontWeight="normal"
rounded="none"
colorScheme="gray"
variant="ghost"
justifyContent="space-between"
bgColor={
keyboardFocusIndex === indexInList
? focusedItemBgColor
: 'transparent'
}
transition="none"
>
<Text noOfLines={0} display="block" pr="2">
{item.name}
</Text>
<HStack>
<IconButton
icon={<EditIcon />}
aria-label="Rename variable"
size="xs"
onClick={handleRenameVariableClick(item)}
/>
<IconButton
icon={<TrashIcon />}
aria-label="Remove variable"
size="xs"
onClick={handleDeleteVariableClick(item)}
/>
</HStack>
</Button>
)
})}
</>
)}
</PopoverContent>
</Portal>
</Popover>
</Flex>
<HStack>
<IconButton
icon={<EditIcon />}
aria-label="Rename variable"
size="xs"
onClick={handleRenameVariableClick(item)}
/>
<IconButton
icon={<TrashIcon />}
aria-label="Remove variable"
size="xs"
onClick={handleDeleteVariableClick(item)}
/>
</HStack>
</Button>
)
})}
</>
)}
</PopoverContent>
</Portal>
</Popover>
</Flex>
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
</FormControl>
)
}