2
0

Add "turn into" option in block context menu

Closes #1302
This commit is contained in:
Baptiste Arnaud
2024-03-05 15:46:28 +01:00
parent 84d6c594af
commit 2fb379b102
16 changed files with 244 additions and 13 deletions

View File

@@ -91,6 +91,7 @@
"tinycolor2": "1.6.0",
"unsplash-js": "7.0.18",
"use-debounce": "9.0.4",
"zod-validation-error": "3.0.3",
"zustand": "4.5.0"
},
"devDependencies": {

View File

@@ -18,7 +18,7 @@ import {
export interface ContextMenuProps<T extends HTMLElement> {
onOpen?: () => void
renderMenu: () => JSX.Element | null
renderMenu: ({ onClose }: { onClose: () => void }) => JSX.Element | null
children: (
ref: MutableRefObject<T | null>,
isOpened: boolean
@@ -101,7 +101,7 @@ export function ContextMenu<T extends HTMLElement = HTMLElement>(
}}
{...props.menuButtonProps}
/>
{props.renderMenu()}
{props.renderMenu({ onClose: onCloseHandler })}
</Menu>
</Portal>
)}

View File

@@ -662,3 +662,12 @@ export const UnlinkIcon = (props: IconProps) => (
<line x1="19" x2="22" y1="16" y2="16" />
</Icon>
)
export const RepeatIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="m2 9 3-3 3 3" />
<path d="M13 18H7a2 2 0 0 1-2-2V6" />
<path d="m22 15-3 3-3-3" />
<path d="M11 6h6a2 2 0 0 1 2 2v10" />
</Icon>
)

View File

@@ -0,0 +1,95 @@
import {
MenuList,
MenuItem,
useDisclosure,
Menu,
MenuButton,
HStack,
Text,
} from '@chakra-ui/react'
import { RepeatIcon, ChevronRightIcon } from '@/components/icons'
import { useDebouncedCallback } from 'use-debounce'
import { useForgedBlock } from '@/features/forge/hooks/useForgedBlock'
import { ForgedBlockIcon } from '@/features/forge/ForgedBlockIcon'
import { ForgedBlock } from '@typebot.io/forge-schemas'
import { TurnableIntoParam } from '@typebot.io/forge'
import { ZodObject } from 'zod'
import { BlockV6 } from '@typebot.io/schemas'
type Props = {
block: BlockV6
onTurnIntoClick: (
params: TurnableIntoParam,
/* eslint-disable @typescript-eslint/no-explicit-any */
blockSchema: ZodObject<any>
) => void
}
export const ForgedBlockTurnIntoMenu = ({ block, onTurnIntoClick }: Props) => {
const { actionDef } = useForgedBlock(
block.type,
'options' in block ? block.options?.action : undefined
)
const { onClose, onOpen, isOpen } = useDisclosure()
const debounceSubMenuClose = useDebouncedCallback(onClose, 200)
const handleMouseEnter = () => {
debounceSubMenuClose.cancel()
onOpen()
}
if (
!actionDef ||
!actionDef?.turnableInto ||
actionDef?.turnableInto.length === 0
)
return null
return (
<Menu isOpen={isOpen} placement="right" offset={[0, 0]} onClose={onClose}>
<MenuButton
as={MenuItem}
onClick={onOpen}
onMouseEnter={handleMouseEnter}
onMouseLeave={debounceSubMenuClose}
icon={<RepeatIcon />}
>
<HStack justifyContent="space-between">
<Text>Turn into</Text>
<ChevronRightIcon />
</HStack>
</MenuButton>
<MenuList
onMouseEnter={handleMouseEnter}
onMouseLeave={debounceSubMenuClose}
>
{actionDef.turnableInto.map((params) => (
<TurnIntoMenuItem
key={params.blockType}
blockType={params.blockType}
onClick={(blockSchema) => onTurnIntoClick(params, blockSchema)}
/>
))}
</MenuList>
</Menu>
)
}
const TurnIntoMenuItem = ({
blockType,
onClick,
}: {
blockType: ForgedBlock['type']
onClick: (blockSchema: ZodObject<any>) => void
}) => {
const { blockDef, blockSchema } = useForgedBlock(blockType)
if (!blockDef || !blockSchema) return null
return (
<MenuItem
icon={<ForgedBlockIcon type={blockType} />}
onClick={() => onClick(blockSchema)}
>
{blockDef.name}
</MenuItem>
)
}

View File

@@ -1,12 +1,9 @@
import { useMemo } from 'react'
import { forgedBlockSchemas, forgedBlocks } from '@typebot.io/forge-schemas'
import { enabledBlocks } from '@typebot.io/forge-repository'
import { BlockWithOptions } from '@typebot.io/schemas'
import { BlockV6 } from '@typebot.io/schemas'
export const useForgedBlock = (
blockType: BlockWithOptions['type'],
action?: string
) =>
export const useForgedBlock = (blockType: BlockV6['type'], action?: string) =>
useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((enabledBlocks as any).includes(blockType) === false) return {}

View File

@@ -45,6 +45,10 @@ import { SettingsModal } from './SettingsModal'
import { TElement } from '@udecode/plate-common'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { useGroupsStore } from '@/features/graph/hooks/useGroupsStore'
import { TurnableIntoParam } from '@typebot.io/forge'
import { ZodError, ZodObject } from 'zod'
import { toast } from 'sonner'
import { fromZodError } from 'zod-validation-error'
export const BlockNode = ({
block,
@@ -186,6 +190,42 @@ export const BlockNode = ({
}
}, [])
const convertBlock = (
turnIntoParams: TurnableIntoParam,
/* eslint-disable @typescript-eslint/no-explicit-any */
targetBlockSchema: ZodObject<any>
) => {
if (!('options' in block) || !block.options) return
const convertedBlockOptions = turnIntoParams.customMapping
? turnIntoParams.customMapping(block.options)
: block.options
try {
updateBlock(
indices,
targetBlockSchema.parse({
...block,
type: turnIntoParams.blockType,
options: {
...convertedBlockOptions,
credentialsId: undefined,
},
} as Block)
)
setOpenedBlockId(block.id)
} catch (error) {
if (error instanceof ZodError) {
const validationError = fromZodError(error)
console.error(validationError)
toast.error('Could not convert block', {
description: validationError.toString(),
})
} else {
toast.error('An error occured while converting the block')
}
}
}
const hasIcomingEdge = typebot?.edges.some((edge) => {
return edge.to.blockId === block.id
})
@@ -198,7 +238,16 @@ export const BlockNode = ({
/>
) : (
<ContextMenu<HTMLDivElement>
renderMenu={() => <BlockNodeContextMenu indices={indices} />}
renderMenu={({ onClose }) => (
<BlockNodeContextMenu
indices={indices}
block={block}
onTurnIntoClick={(params, schema) => {
convertBlock(params, schema)
onClose()
}}
/>
)}
>
{(ref, isContextMenuOpened) => (
<Popover

View File

@@ -1,11 +1,24 @@
import { MenuList, MenuItem } from '@chakra-ui/react'
import { CopyIcon, TrashIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { BlockIndices } from '@typebot.io/schemas'
import { BlockIndices, BlockV6 } from '@typebot.io/schemas'
import { useTranslate } from '@tolgee/react'
import { ForgedBlockTurnIntoMenu } from '@/features/forge/components/ForgedBlockTurnIntoMenu'
import { TurnableIntoParam } from '@typebot.io/forge'
import { ZodObject } from 'zod'
type Props = { indices: BlockIndices }
export const BlockNodeContextMenu = ({ indices }: Props) => {
type Props = {
indices: BlockIndices
block: BlockV6
/* eslint-disable @typescript-eslint/no-explicit-any */
onTurnIntoClick: (params: TurnableIntoParam, schema: ZodObject<any>) => void
}
export const BlockNodeContextMenu = ({
indices,
block,
onTurnIntoClick,
}: Props) => {
const { t } = useTranslate()
const { deleteBlock, duplicateBlock } = useTypebot()
@@ -15,6 +28,10 @@ export const BlockNodeContextMenu = ({ indices }: Props) => {
return (
<MenuList>
<ForgedBlockTurnIntoMenu
block={block}
onTurnIntoClick={onTurnIntoClick}
/>
<MenuItem icon={<CopyIcon />} onClick={handleDuplicateClick}>
{t('duplicate')}
</MenuItem>