✨ Introducing The Forge (#1072)
The Forge allows anyone to easily create their own Typebot Block. Closes #380
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export const TextInput = forwardRef(function TextInput(
|
||||
) : (
|
||||
Input
|
||||
)}
|
||||
{helperText && <FormHelperText>{helperText}</FormHelperText>}
|
||||
{helperText && <FormHelperText mt="0">{helperText}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user