feat(steps): ✨ Add Embed bubble
This commit is contained in:
@ -13,6 +13,7 @@ import {
|
||||
FlagIcon,
|
||||
GlobeIcon,
|
||||
ImageIcon,
|
||||
LayoutIcon,
|
||||
NumberIcon,
|
||||
PhoneIcon,
|
||||
SendEmailIcon,
|
||||
@ -39,6 +40,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
return <ImageIcon color="blue.500" {...props} />
|
||||
case BubbleStepType.VIDEO:
|
||||
return <FilmIcon color="blue.500" {...props} />
|
||||
case BubbleStepType.EMBED:
|
||||
return <LayoutIcon color="blue.500" {...props} />
|
||||
case InputStepType.TEXT:
|
||||
return <TextIcon color="orange.500" {...props} />
|
||||
case InputStepType.NUMBER:
|
||||
|
@ -19,6 +19,12 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
return <Text>Image</Text>
|
||||
case BubbleStepType.VIDEO:
|
||||
return <Text>Video</Text>
|
||||
case BubbleStepType.EMBED:
|
||||
return (
|
||||
<Tooltip label="Embed a pdf, an iframe, a website...">
|
||||
<Text>Embed</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case InputStepType.NUMBER:
|
||||
return <Text>Number</Text>
|
||||
case InputStepType.EMAIL:
|
||||
|
@ -0,0 +1,51 @@
|
||||
import { HStack, Stack, Text } from '@chakra-ui/react'
|
||||
import { SmartNumberInput } from 'components/shared/SmartNumberInput'
|
||||
import { Input } from 'components/shared/Textbox/Input'
|
||||
import { EmbedBubbleContent } from 'models'
|
||||
import { sanitizeUrl } from 'utils'
|
||||
|
||||
type Props = {
|
||||
content: EmbedBubbleContent
|
||||
onSubmit: (content: EmbedBubbleContent) => void
|
||||
}
|
||||
|
||||
export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
|
||||
const handleUrlChange = (url: string) => {
|
||||
let iframeUrl = sanitizeUrl(
|
||||
url.trim().startsWith('<iframe') ? extractUrlFromIframe(url) : url
|
||||
)
|
||||
if (iframeUrl.endsWith('.pdf')) {
|
||||
iframeUrl = `https://docs.google.com/viewer?embedded=true&url=${iframeUrl}`
|
||||
}
|
||||
onSubmit({ ...content, url: iframeUrl })
|
||||
}
|
||||
|
||||
const handleHeightChange = (height?: number) =>
|
||||
height && onSubmit({ ...content, height })
|
||||
|
||||
return (
|
||||
<Stack p="2" spacing={6}>
|
||||
<Stack>
|
||||
<Input
|
||||
placeholder="Paste the link or code..."
|
||||
defaultValue={content?.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
<Text fontSize="sm" color="gray.400" textAlign="center">
|
||||
Works with PDFs, iframes, websites...
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<HStack justify="space-between">
|
||||
<Text>Height: </Text>
|
||||
<SmartNumberInput
|
||||
value={content?.height}
|
||||
onValueChange={handleHeightChange}
|
||||
/>
|
||||
</HStack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const extractUrlFromIframe = (iframe: string) =>
|
||||
[...iframe.matchAll(/src="([^"]+)"/g)][0][1]
|
@ -12,6 +12,7 @@ import {
|
||||
TextBubbleStep,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
import { EmbedUploadContent } from './EmbedUploadContent'
|
||||
import { VideoUploadContent } from './VideoUploadContent'
|
||||
|
||||
type Props = {
|
||||
@ -25,7 +26,10 @@ export const MediaBubblePopoverContent = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<PopoverContent onMouseDown={handleMouseDown} w="500px">
|
||||
<PopoverContent
|
||||
onMouseDown={handleMouseDown}
|
||||
w={props.step.type === BubbleStepType.IMAGE ? '500px' : '400px'}
|
||||
>
|
||||
<PopoverArrow />
|
||||
<PopoverBody ref={ref} shadow="lg">
|
||||
<MediaBubbleContent {...props} />
|
||||
@ -52,5 +56,10 @@ export const MediaBubbleContent = ({ step, onContentChange }: Props) => {
|
||||
<VideoUploadContent content={step.content} onSubmit={onContentChange} />
|
||||
)
|
||||
}
|
||||
case BubbleStepType.EMBED: {
|
||||
return (
|
||||
<EmbedUploadContent content={step.content} onSubmit={onContentChange} />
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { isChoiceInput, isInputStep } from 'utils'
|
||||
import { ItemNodesList } from '../../ItemNode'
|
||||
import {
|
||||
EmbedBubbleContent,
|
||||
SetVariableContent,
|
||||
TextBubbleContent,
|
||||
VideoBubbleContent,
|
||||
@ -42,6 +43,9 @@ export const StepNodeContent = ({ step, indices }: Props) => {
|
||||
case BubbleStepType.VIDEO: {
|
||||
return <VideoBubbleContent step={step} />
|
||||
}
|
||||
case BubbleStepType.EMBED: {
|
||||
return <EmbedBubbleContent step={step} />
|
||||
}
|
||||
case InputStepType.TEXT: {
|
||||
return (
|
||||
<PlaceholderContent
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { Box, Text } from '@chakra-ui/react'
|
||||
import { EmbedBubbleStep } from 'models'
|
||||
|
||||
export const EmbedBubbleContent = ({ step }: { step: EmbedBubbleStep }) => {
|
||||
if (!step.content?.url) return <Text color="gray.500">Click to edit...</Text>
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<iframe
|
||||
id="embed-bubble-content"
|
||||
src={step.content.url}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -3,3 +3,4 @@ export * from './WithVariableContent'
|
||||
export * from './VideoBubbleContent'
|
||||
export * from './WebhookContent'
|
||||
export * from './TextBubbleContent'
|
||||
export * from './EmbedBubbleContent'
|
||||
|
74
apps/builder/playwright/tests/bubbles/embed.spec.ts
Normal file
74
apps/builder/playwright/tests/bubbles/embed.spec.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import {
|
||||
createTypebots,
|
||||
parseDefaultBlockWithStep,
|
||||
} from '../../services/database'
|
||||
import { BubbleStepType, defaultEmbedBubbleContent } from 'models'
|
||||
import { typebotViewer } from '../../services/selectorUtils'
|
||||
import cuid from 'cuid'
|
||||
|
||||
const pdfSrc = 'https://www.orimi.com/pdf-test.pdf'
|
||||
const iframeCode = '<iframe src="https://typebot.io"></iframe>'
|
||||
const siteSrc = 'https://app.cal.com/baptistearno/15min'
|
||||
|
||||
test.describe.parallel('Embed bubble step', () => {
|
||||
test.describe('Content settings', () => {
|
||||
test('should import and parse embed correctly', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
...parseDefaultBlockWithStep({
|
||||
type: BubbleStepType.EMBED,
|
||||
content: defaultEmbedBubbleContent,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Click to edit...')
|
||||
await page.fill('input[placeholder="Paste the link or code..."]', pdfSrc)
|
||||
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
|
||||
'src',
|
||||
`https://docs.google.com/viewer?embedded=true&url=${pdfSrc}`
|
||||
)
|
||||
await page.fill(
|
||||
'input[placeholder="Paste the link or code..."]',
|
||||
iframeCode
|
||||
)
|
||||
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
|
||||
'src',
|
||||
'https://typebot.io'
|
||||
)
|
||||
await page.fill('input[placeholder="Paste the link or code..."]', siteSrc)
|
||||
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
|
||||
'src',
|
||||
siteSrc
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Preview', () => {
|
||||
test('should display embed correctly', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
...parseDefaultBlockWithStep({
|
||||
type: BubbleStepType.EMBED,
|
||||
content: {
|
||||
url: siteSrc,
|
||||
height: 700,
|
||||
},
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator('iframe#embed-bubble-content')
|
||||
).toHaveAttribute('src', siteSrc)
|
||||
})
|
||||
})
|
||||
})
|
@ -34,6 +34,7 @@ import {
|
||||
ItemType,
|
||||
defaultConditionContent,
|
||||
defaultSendEmailOptions,
|
||||
defaultEmbedBubbleContent,
|
||||
} from 'models'
|
||||
import { Typebot } from 'models'
|
||||
import useSWR from 'swr'
|
||||
@ -250,6 +251,8 @@ const parseDefaultContent = (type: BubbleStepType): BubbleStepContent => {
|
||||
return defaultImageBubbleContent
|
||||
case BubbleStepType.VIDEO:
|
||||
return defaultVideoBubbleContent
|
||||
case BubbleStepType.EMBED:
|
||||
return defaultEmbedBubbleContent
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,8 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"composite": true
|
||||
"composite": true,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
Reference in New Issue
Block a user