📝 Add Contribute docs
This commit is contained in:
@@ -51,7 +51,7 @@ export const DropdownList = <T extends readonly any[]>({
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
width={props.width === 'full' || label ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
|
||||
135
apps/builder/src/components/PrimitiveList.tsx
Normal file
135
apps/builder/src/components/PrimitiveList.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
|
||||
import { TrashIcon, PlusIcon } from '@/components/icons'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
type ItemWithId<T extends number | string | boolean> = { id: string; value?: T }
|
||||
|
||||
export type TableListItemProps<T> = {
|
||||
item: T
|
||||
onItemChange: (item: T) => void
|
||||
}
|
||||
|
||||
type Props<T extends number | string | boolean> = {
|
||||
initialItems?: T[]
|
||||
addLabel?: string
|
||||
newItemDefaultProps?: Partial<T>
|
||||
hasDefaultItem?: boolean
|
||||
ComponentBetweenItems?: (props: unknown) => JSX.Element
|
||||
onItemsChange: (items: T[]) => void
|
||||
children: (props: TableListItemProps<T>) => JSX.Element
|
||||
}
|
||||
|
||||
const addIdToItems = <T extends number | string | boolean>(
|
||||
items: T[]
|
||||
): ItemWithId<T>[] => items.map((item) => ({ id: createId(), value: item }))
|
||||
|
||||
const removeIdFromItems = <T extends number | string | boolean>(
|
||||
items: ItemWithId<T>[]
|
||||
): T[] => items.map((item) => item.value as T)
|
||||
|
||||
export const PrimitiveList = <T extends number | string | boolean>({
|
||||
initialItems,
|
||||
addLabel = 'Add',
|
||||
hasDefaultItem,
|
||||
children,
|
||||
ComponentBetweenItems,
|
||||
onItemsChange,
|
||||
}: Props<T>) => {
|
||||
const [items, setItems] = useState<ItemWithId<T>[]>()
|
||||
const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (items) return
|
||||
if (initialItems) {
|
||||
setItems(addIdToItems(initialItems))
|
||||
} else if (hasDefaultItem) {
|
||||
setItems(addIdToItems([]))
|
||||
} else {
|
||||
setItems([])
|
||||
}
|
||||
}, [hasDefaultItem, initialItems, items])
|
||||
|
||||
const createItem = () => {
|
||||
if (!items) return
|
||||
const newItems = [...items, { id: createId() }]
|
||||
setItems(newItems)
|
||||
onItemsChange(removeIdFromItems(newItems))
|
||||
}
|
||||
|
||||
const updateItem = (itemIndex: number, newValue: T) => {
|
||||
if (!items) return
|
||||
const newItems = items.map((item, idx) =>
|
||||
idx === itemIndex ? { ...item, value: newValue } : item
|
||||
)
|
||||
setItems(newItems)
|
||||
onItemsChange(removeIdFromItems(newItems))
|
||||
}
|
||||
|
||||
const deleteItem = (itemIndex: number) => () => {
|
||||
if (!items) return
|
||||
const newItems = [...items]
|
||||
newItems.splice(itemIndex, 1)
|
||||
setItems([...newItems])
|
||||
onItemsChange(removeIdFromItems([...newItems]))
|
||||
}
|
||||
|
||||
const handleMouseEnter = (itemIndex: number) => () =>
|
||||
setShowDeleteIndex(itemIndex)
|
||||
|
||||
const handleCellChange = (itemIndex: number) => (item: T) =>
|
||||
updateItem(itemIndex, item)
|
||||
|
||||
const handleMouseLeave = () => setShowDeleteIndex(null)
|
||||
|
||||
return (
|
||||
<Stack spacing={0}>
|
||||
{items?.map((item, itemIndex) => (
|
||||
<Box key={item.id}>
|
||||
{itemIndex !== 0 && ComponentBetweenItems && (
|
||||
<ComponentBetweenItems />
|
||||
)}
|
||||
<Flex
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(itemIndex)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
mt={itemIndex !== 0 && ComponentBetweenItems ? 4 : 0}
|
||||
justifyContent="center"
|
||||
pb="4"
|
||||
>
|
||||
{children({
|
||||
item: item.value as T,
|
||||
onItemChange: handleCellChange(itemIndex),
|
||||
})}
|
||||
<Fade
|
||||
in={showDeleteIndex === itemIndex}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '-15px',
|
||||
top: '-15px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove cell"
|
||||
onClick={deleteItem(itemIndex)}
|
||||
size="sm"
|
||||
shadow="md"
|
||||
/>
|
||||
</Fade>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
onClick={createItem}
|
||||
flexShrink={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
{addLabel}
|
||||
</Button>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export const NumberInput = <HasVariable extends boolean>({
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
isRequired={isRequired}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
width={label || props.width === 'full' ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
|
||||
@@ -35,6 +35,7 @@ export type TextInputProps = {
|
||||
placeholder?: string
|
||||
isDisabled?: boolean
|
||||
direction?: 'row' | 'column'
|
||||
width?: 'full'
|
||||
} & Pick<
|
||||
InputProps,
|
||||
| 'autoComplete'
|
||||
@@ -66,6 +67,7 @@ export const TextInput = forwardRef(function TextInput(
|
||||
size,
|
||||
maxWidth,
|
||||
direction = 'column',
|
||||
width,
|
||||
}: TextInputProps,
|
||||
ref
|
||||
) {
|
||||
@@ -141,7 +143,7 @@ export const TextInput = forwardRef(function TextInput(
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
width={label || width === 'full' ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
|
||||
@@ -28,7 +28,7 @@ type Props = {
|
||||
helperText?: ReactNode
|
||||
onChange: (value: string) => void
|
||||
direction?: 'row' | 'column'
|
||||
} & Pick<TextareaProps, 'minH'>
|
||||
} & Pick<TextareaProps, 'minH' | 'width'>
|
||||
|
||||
export const Textarea = ({
|
||||
id,
|
||||
@@ -43,6 +43,7 @@ export const Textarea = ({
|
||||
minH,
|
||||
helperText,
|
||||
direction = 'column',
|
||||
width,
|
||||
}: Props) => {
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const [isTouched, setIsTouched] = useState(false)
|
||||
@@ -108,7 +109,7 @@ export const Textarea = ({
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
width={label || width === 'full' ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
|
||||
@@ -46,6 +46,7 @@ type Props = {
|
||||
helperText?: ReactNode
|
||||
moreInfoTooltip?: string
|
||||
direction?: 'row' | 'column'
|
||||
width?: 'full'
|
||||
} & Omit<InputProps, 'placeholder'>
|
||||
|
||||
export const VariableSearchInput = ({
|
||||
@@ -58,6 +59,7 @@ export const VariableSearchInput = ({
|
||||
moreInfoTooltip,
|
||||
direction = 'column',
|
||||
isRequired,
|
||||
width,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
|
||||
@@ -196,7 +198,7 @@ export const VariableSearchInput = ({
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
width={label || width === 'full' ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
|
||||
@@ -25,6 +25,7 @@ type Props = {
|
||||
moreInfoTooltip?: string
|
||||
direction?: 'row' | 'column'
|
||||
isRequired?: boolean
|
||||
width?: 'full'
|
||||
onChange: (value: string | undefined) => void
|
||||
}
|
||||
export const ForgeSelectInput = ({
|
||||
@@ -38,6 +39,7 @@ export const ForgeSelectInput = ({
|
||||
moreInfoTooltip,
|
||||
isRequired,
|
||||
direction = 'column',
|
||||
width,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
@@ -83,7 +85,7 @@ export const ForgeSelectInput = ({
|
||||
isRequired={isRequired}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
width={label || width === 'full' ? 'full' : 'auto'}
|
||||
spacing={direction === 'column' ? 2 : 3}
|
||||
>
|
||||
{label && (
|
||||
|
||||
@@ -31,10 +31,8 @@ export const ZodDiscriminatedUnionLayout = ({
|
||||
currentItem={data?.[discriminant]}
|
||||
onItemSelect={(item) => onDataChange({ ...data, [discriminant]: item })}
|
||||
items={
|
||||
[...schema._def.optionsMap.keys()].filter(
|
||||
(key) =>
|
||||
isDefined(key) &&
|
||||
!schema._def.optionsMap.get(key)?._def.layout?.isHidden
|
||||
[...schema._def.optionsMap.keys()].filter((key) =>
|
||||
isDefined(key)
|
||||
) as string[]
|
||||
}
|
||||
placeholder={dropdownPlaceholder}
|
||||
|
||||
@@ -13,12 +13,15 @@ import {
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
FormLabel,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
import { PrimitiveList } from '@/components/PrimitiveList'
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
|
||||
const mdComponents = {
|
||||
a: ({ href, children }) => (
|
||||
@@ -40,6 +43,8 @@ export const ZodFieldLayout = ({
|
||||
isInAccordion,
|
||||
blockDef,
|
||||
blockOptions,
|
||||
width,
|
||||
propName,
|
||||
onDataChange,
|
||||
}: {
|
||||
data: any
|
||||
@@ -47,6 +52,8 @@ export const ZodFieldLayout = ({
|
||||
isInAccordion?: boolean
|
||||
blockDef?: ForgedBlockDefinition
|
||||
blockOptions?: ForgedBlock['options']
|
||||
width?: 'full'
|
||||
propName?: string
|
||||
onDataChange: (val: any) => void
|
||||
}) => {
|
||||
const layout = schema._def.layout as ZodLayoutMetadata<ZodTypeAny> | undefined
|
||||
@@ -54,7 +61,6 @@ export const ZodFieldLayout = ({
|
||||
? schema._def.innerType._def.typeName
|
||||
: schema._def.typeName
|
||||
|
||||
if (layout?.isHidden) return null
|
||||
switch (type) {
|
||||
case 'ZodObject':
|
||||
return (
|
||||
@@ -125,6 +131,7 @@ export const ZodFieldLayout = ({
|
||||
moreInfoTooltip={layout?.moreInfoTooltip}
|
||||
placeholder={layout?.placeholder}
|
||||
direction={layout?.direction}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -143,6 +150,18 @@ export const ZodFieldLayout = ({
|
||||
isRequired={layout?.isRequired}
|
||||
moreInfoTooltip={layout?.moreInfoTooltip}
|
||||
onValueChange={onDataChange}
|
||||
direction={layout?.direction}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'ZodBoolean': {
|
||||
return (
|
||||
<SwitchWithLabel
|
||||
label={layout?.label ?? propName ?? ''}
|
||||
initialValue={data ?? layout?.defaultValue}
|
||||
onCheckChange={onDataChange}
|
||||
moreInfoContent={layout?.moreInfoTooltip}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -166,10 +185,11 @@ export const ZodFieldLayout = ({
|
||||
}
|
||||
moreInfoTooltip={layout?.moreInfoTooltip}
|
||||
onChange={onDataChange}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (layout?.input === 'variableDropdown') {
|
||||
if (layout?.inputType === 'variableDropdown') {
|
||||
return (
|
||||
<VariableSearchInput
|
||||
initialVariableId={data}
|
||||
@@ -184,10 +204,11 @@ export const ZodFieldLayout = ({
|
||||
</Markdown>
|
||||
) : undefined
|
||||
}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (layout?.input === 'textarea') {
|
||||
if (layout?.inputType === 'textarea') {
|
||||
return (
|
||||
<Textarea
|
||||
defaultValue={data ?? layout?.defaultValue}
|
||||
@@ -204,6 +225,7 @@ export const ZodFieldLayout = ({
|
||||
withVariableButton={layout?.withVariableButton}
|
||||
moreInfoTooltip={layout.moreInfoTooltip}
|
||||
onChange={onDataChange}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -217,11 +239,12 @@ export const ZodFieldLayout = ({
|
||||
<Markdown components={mdComponents}>{layout.helperText}</Markdown>
|
||||
) : undefined
|
||||
}
|
||||
type={layout?.input === 'password' ? 'password' : undefined}
|
||||
type={layout?.inputType === 'password' ? 'password' : undefined}
|
||||
isRequired={layout?.isRequired}
|
||||
withVariableButton={layout?.withVariableButton}
|
||||
moreInfoTooltip={layout?.moreInfoTooltip}
|
||||
onChange={onDataChange}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -240,24 +263,54 @@ const ZodArrayContent = ({
|
||||
layout: ZodLayoutMetadata<ZodTypeAny> | undefined
|
||||
isInAccordion?: boolean
|
||||
onDataChange: (val: any) => void
|
||||
}) => (
|
||||
<TableList
|
||||
onItemsChange={(items) => {
|
||||
onDataChange(items)
|
||||
}}
|
||||
initialItems={data}
|
||||
addLabel={`Add ${layout?.itemLabel ?? ''}`}
|
||||
isOrdered={layout?.isOrdered}
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<ZodFieldLayout
|
||||
schema={schema._def.innerType._def.type}
|
||||
data={item}
|
||||
isInAccordion={isInAccordion}
|
||||
onDataChange={onItemChange}
|
||||
/>
|
||||
}) => {
|
||||
const type = schema._def.innerType
|
||||
? schema._def.innerType._def.typeName
|
||||
: schema._def.typeName
|
||||
if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum')
|
||||
return (
|
||||
<Stack spacing={0}>
|
||||
{layout?.label && <FormLabel>{layout.label}</FormLabel>}
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<PrimitiveList
|
||||
onItemsChange={(items) => {
|
||||
onDataChange(items)
|
||||
}}
|
||||
initialItems={data}
|
||||
addLabel={`Add ${layout?.itemLabel ?? ''}`}
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<ZodFieldLayout
|
||||
schema={schema._def.innerType._def.type}
|
||||
data={item}
|
||||
isInAccordion={isInAccordion}
|
||||
onDataChange={onItemChange}
|
||||
width="full"
|
||||
/>
|
||||
)}
|
||||
</PrimitiveList>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</TableList>
|
||||
)
|
||||
)
|
||||
return (
|
||||
<TableList
|
||||
onItemsChange={(items) => {
|
||||
onDataChange(items)
|
||||
}}
|
||||
initialItems={data}
|
||||
addLabel={`Add ${layout?.itemLabel ?? ''}`}
|
||||
isOrdered={layout?.isOrdered}
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<ZodFieldLayout
|
||||
schema={schema._def.innerType._def.type}
|
||||
data={item}
|
||||
isInAccordion={isInAccordion}
|
||||
onDataChange={onItemChange}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</TableList>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ export const ZodObjectLayout = ({
|
||||
data={data?.[key]}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
propName={key}
|
||||
onDataChange={(val) => onDataChange({ ...data, [key]: val })}
|
||||
/>,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user