@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user