✨ Introduce bot v2 in builder (#328)
Also, the new engine is the default for updated typebots for viewer Closes #211
This commit is contained in:
@ -1,9 +0,0 @@
|
||||
.cm-editor {
|
||||
outline: 0px solid transparent !important;
|
||||
height: 100%;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
border-radius: 5px;
|
||||
}
|
@ -1,29 +1,26 @@
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
Fade,
|
||||
HStack,
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { EditorView, basicSetup } from 'codemirror'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { json, jsonParseLinter } from '@codemirror/lang-json'
|
||||
import { css } from '@codemirror/lang-css'
|
||||
import { javascript } from '@codemirror/lang-javascript'
|
||||
import { html } from '@codemirror/lang-html'
|
||||
import { oneDark } from '@codemirror/theme-one-dark'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { linter, LintSource } from '@codemirror/lint'
|
||||
import { VariablesButton } from '@/features/variables'
|
||||
import { Variable } from 'models'
|
||||
import { env } from 'utils'
|
||||
|
||||
const linterExtension = linter(jsonParseLinter() as unknown as LintSource)
|
||||
import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror'
|
||||
import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night'
|
||||
import { githubLight } from '@uiw/codemirror-theme-github'
|
||||
import { LanguageName, loadLanguage } from '@uiw/codemirror-extensions-langs'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { CopyButton } from './CopyButton'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
lang?: 'css' | 'json' | 'js' | 'html'
|
||||
value?: string
|
||||
defaultValue?: string
|
||||
lang: LanguageName
|
||||
isReadOnly?: boolean
|
||||
debounceTimeout?: number
|
||||
withVariableButton?: boolean
|
||||
@ -31,7 +28,7 @@ type Props = {
|
||||
onChange?: (value: string) => void
|
||||
}
|
||||
export const CodeEditor = ({
|
||||
value,
|
||||
defaultValue,
|
||||
lang,
|
||||
onChange,
|
||||
height = '250px',
|
||||
@ -40,91 +37,25 @@ export const CodeEditor = ({
|
||||
debounceTimeout = 1000,
|
||||
...props
|
||||
}: Props & Omit<BoxProps, 'onChange'>) => {
|
||||
const isDark = useColorMode().colorMode === 'dark'
|
||||
const editorContainer = useRef<HTMLDivElement | null>(null)
|
||||
const editorView = useRef<EditorView | null>(null)
|
||||
const [, setPlainTextValue] = useState(value)
|
||||
const theme = useColorModeValue(githubLight, tokyoNight)
|
||||
const codeEditor = useRef<ReactCodeMirrorRef | null>(null)
|
||||
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||
const isVariableButtonDisplayed = withVariableButton && !isReadOnly
|
||||
const [value, _setValue] = useState(defaultValue ?? '')
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
|
||||
const debounced = useDebouncedCallback(
|
||||
const setValue = useDebouncedCallback(
|
||||
(value) => {
|
||||
setPlainTextValue(value)
|
||||
_setValue(value)
|
||||
onChange && onChange(value)
|
||||
},
|
||||
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
debounced.flush()
|
||||
},
|
||||
[debounced]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorView.current || !isReadOnly) return
|
||||
editorView.current.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editorView.current.state.doc.length,
|
||||
insert: value,
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
const editor = initEditor(value)
|
||||
return () => {
|
||||
editor?.destroy()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const initEditor = (value: string) => {
|
||||
if (!editorContainer.current) return
|
||||
const updateListenerExtension = EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && onChange)
|
||||
debounced(update.state.doc.toJSON().join('\n'))
|
||||
})
|
||||
const extensions = [
|
||||
updateListenerExtension,
|
||||
basicSetup,
|
||||
EditorState.readOnly.of(isReadOnly),
|
||||
]
|
||||
if (isDark) extensions.push(oneDark)
|
||||
if (lang === 'json') {
|
||||
extensions.push(json())
|
||||
extensions.push(linterExtension)
|
||||
}
|
||||
if (lang === 'css') extensions.push(css())
|
||||
if (lang === 'js') extensions.push(javascript())
|
||||
if (lang === 'html') extensions.push(html())
|
||||
extensions.push(
|
||||
EditorView.theme({
|
||||
'&': { maxHeight: '500px' },
|
||||
'.cm-gutter,.cm-content': { minHeight: isReadOnly ? '0' : height },
|
||||
'.cm-scroller': { overflow: 'auto' },
|
||||
})
|
||||
)
|
||||
const editor = new EditorView({
|
||||
state: EditorState.create({
|
||||
extensions,
|
||||
}),
|
||||
parent: editorContainer.current,
|
||||
})
|
||||
editor.dispatch({
|
||||
changes: { from: 0, insert: value },
|
||||
})
|
||||
editorView.current = editor
|
||||
return editor
|
||||
}
|
||||
|
||||
const handleVariableSelected = (variable?: Pick<Variable, 'id' | 'name'>) => {
|
||||
editorView.current?.focus()
|
||||
codeEditor.current?.view?.focus()
|
||||
const insert = `{{${variable?.name}}}`
|
||||
editorView.current?.dispatch({
|
||||
codeEditor.current?.view?.dispatch({
|
||||
changes: {
|
||||
from: carretPosition,
|
||||
insert,
|
||||
@ -133,9 +64,10 @@ export const CodeEditor = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleKeyUp = () => {
|
||||
if (!editorContainer.current) return
|
||||
setCarretPosition(editorView.current?.state.selection.main.from ?? 0)
|
||||
const handleChange = (newValue: string) => {
|
||||
if (isDefined(props.value)) return
|
||||
setValue(newValue)
|
||||
setCarretPosition(codeEditor.current?.state?.selection.main.head ?? 0)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -143,19 +75,61 @@ export const CodeEditor = ({
|
||||
align="flex-end"
|
||||
spacing={0}
|
||||
borderWidth={'1px'}
|
||||
borderRadius="md"
|
||||
bg={useColorModeValue('#FCFCFC', '#282C34')}
|
||||
rounded="md"
|
||||
bg={useColorModeValue('white', '#1A1B26')}
|
||||
width="full"
|
||||
h="full"
|
||||
pos="relative"
|
||||
onMouseEnter={onOpen}
|
||||
onMouseLeave={onClose}
|
||||
sx={{
|
||||
'& .cm-editor': {
|
||||
maxH: '70vh',
|
||||
outline: '0px solid transparent !important',
|
||||
rounded: 'md',
|
||||
},
|
||||
'& .cm-scroller': {
|
||||
rounded: 'md',
|
||||
overflow: 'auto',
|
||||
},
|
||||
'& .cm-gutter,.cm-content': {
|
||||
minH: isReadOnly ? '0' : height,
|
||||
},
|
||||
'& .ͼ1 .cm-scroller': {
|
||||
fontSize: '14px',
|
||||
fontFamily:
|
||||
'JetBrainsMono, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
w={isVariableButtonDisplayed ? 'calc(100% - 32px)' : '100%'}
|
||||
ref={editorContainer}
|
||||
<CodeMirror
|
||||
data-testid="code-editor"
|
||||
{...props}
|
||||
onKeyUp={handleKeyUp}
|
||||
ref={codeEditor}
|
||||
value={props.value ?? value}
|
||||
onChange={handleChange}
|
||||
theme={theme}
|
||||
extensions={[loadLanguage(lang)].filter(isDefined)}
|
||||
editable={!isReadOnly}
|
||||
style={{
|
||||
width: isVariableButtonDisplayed ? 'calc(100% - 32px)' : '100%',
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{isVariableButtonDisplayed && (
|
||||
<VariablesButton onSelectVariable={handleVariableSelected} size="sm" />
|
||||
)}
|
||||
{isReadOnly && (
|
||||
<Fade in={isOpen}>
|
||||
<CopyButton
|
||||
textToCopy={props.value ?? value}
|
||||
pos="absolute"
|
||||
right={0.5}
|
||||
top={0.5}
|
||||
size="xs"
|
||||
colorScheme="blue"
|
||||
/>
|
||||
</Fade>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
@ -29,12 +29,12 @@ const colorsSelection: `#${string}`[] = [
|
||||
]
|
||||
|
||||
type Props = {
|
||||
initialColor: string
|
||||
initialColor?: string
|
||||
onColorChange: (color: string) => void
|
||||
}
|
||||
|
||||
export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
||||
const [color, setColor] = useState(initialColor)
|
||||
const [color, setColor] = useState(initialColor ?? '')
|
||||
|
||||
useEffect(() => {
|
||||
onColorChange(color)
|
||||
|
@ -2,7 +2,6 @@ import Head from 'next/head'
|
||||
|
||||
export const Seo = ({
|
||||
title,
|
||||
currentUrl = 'https://app.typebot.io',
|
||||
description = 'Create and publish conversational forms that collect 4 times more answers and feel native to your product',
|
||||
imagePreviewUrl = 'https://app.typebot.io/site-preview.png',
|
||||
}: {
|
||||
@ -20,9 +19,6 @@ export const Seo = ({
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
|
||||
<meta property="twitter:url" content={currentUrl} />
|
||||
<meta property="og:url" content={currentUrl} />
|
||||
|
||||
<meta name="description" content={description} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="og:description" content={description} />
|
||||
|
@ -1,43 +1,35 @@
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { useUser } from '@/features/account'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { initBubble } from 'typebot-js'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
import React from 'react'
|
||||
import { Bubble } from '@typebot.io/react'
|
||||
import { planToReadable } from '@/features/billing'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
|
||||
export const SupportBubble = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const { user } = useUser()
|
||||
const { workspace } = useWorkspace()
|
||||
const [localTypebotId, setLocalTypebotId] = useState(typebot?.id)
|
||||
const [localUserId, setLocalUserId] = useState(user?.id)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isCloudProdInstance &&
|
||||
(localTypebotId !== typebot?.id || localUserId !== user?.id)
|
||||
) {
|
||||
setLocalTypebotId(typebot?.id)
|
||||
setLocalUserId(user?.id)
|
||||
initBubble({
|
||||
url: `https://viewer.typebot.io/typebot-support`,
|
||||
backgroundColor: '#ffffff',
|
||||
button: {
|
||||
color: '#0042DA',
|
||||
},
|
||||
hiddenVariables: {
|
||||
'User ID': user?.id,
|
||||
'First name': user?.name?.split(' ')[0] ?? undefined,
|
||||
Email: user?.email ?? undefined,
|
||||
'Typebot ID': typebot?.id,
|
||||
'Avatar URL': user?.image ?? undefined,
|
||||
Plan: planToReadable(workspace?.plan),
|
||||
},
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user, typebot])
|
||||
if (!isCloudProdInstance) return null
|
||||
|
||||
return <></>
|
||||
return (
|
||||
<Bubble
|
||||
apiHost="https://viewer.typebot.io"
|
||||
typebot="typebot-support"
|
||||
prefilledVariables={{
|
||||
'User ID': user?.id,
|
||||
'First name': user?.name?.split(' ')[0] ?? undefined,
|
||||
Email: user?.email ?? undefined,
|
||||
'Typebot ID': typebot?.id,
|
||||
'Avatar URL': user?.image ?? undefined,
|
||||
Plan: planToReadable(workspace?.plan),
|
||||
}}
|
||||
theme={{
|
||||
chatWindow: {
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -93,6 +93,7 @@ export const SmartNumberInput = <HasVariable extends boolean>({
|
||||
as={HStack}
|
||||
isRequired={isRequired}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
>
|
||||
{label && (
|
||||
<FormLabel mb="0" flexShrink={0}>
|
||||
|
@ -5,12 +5,12 @@ export const mockedUser: User = {
|
||||
name: 'John Doe',
|
||||
email: 'user@email.com',
|
||||
company: null,
|
||||
createdAt: new Date(),
|
||||
createdAt: new Date('2022-01-01'),
|
||||
emailVerified: null,
|
||||
graphNavigation: 'TRACKPAD',
|
||||
preferredAppAppearance: null,
|
||||
image: 'https://avatars.githubusercontent.com/u/16015833?v=4',
|
||||
lastActivityAt: new Date(),
|
||||
lastActivityAt: new Date('2022-01-01'),
|
||||
onboardingCategories: [],
|
||||
updatedAt: new Date(),
|
||||
updatedAt: new Date('2022-01-01'),
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { BubbleBlockType, defaultAudioBubbleContent } from 'models'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
|
||||
const audioSampleUrl =
|
||||
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
||||
@ -34,7 +33,7 @@ test('should work as expected', async ({ page }) => {
|
||||
RegExp(`/public/typebots/${typebotId}/blocks`, 'gm')
|
||||
)
|
||||
await page.getByRole('button', { name: 'Preview', exact: true }).click()
|
||||
await expect(typebotViewer(page).locator('audio')).toHaveAttribute(
|
||||
await expect(page.locator('audio')).toHaveAttribute(
|
||||
'src',
|
||||
RegExp(`/public/typebots/${typebotId}/blocks`, 'gm')
|
||||
)
|
||||
|
@ -3,7 +3,6 @@ import { BubbleBlockType, defaultEmbedBubbleContent } from 'models'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
|
||||
const pdfSrc = 'https://www.orimi.com/pdf-test.pdf'
|
||||
const siteSrc = 'https://app.cal.com/baptistearno/15min'
|
||||
@ -47,9 +46,10 @@ test.describe.parallel('Embed bubble block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator('iframe#embed-bubble-content')
|
||||
).toHaveAttribute('src', siteSrc)
|
||||
await expect(page.locator('iframe#embed-bubble-content')).toHaveAttribute(
|
||||
'src',
|
||||
siteSrc
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -3,7 +3,6 @@ import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { BubbleBlockType, defaultImageBubbleContent } from 'models'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
const unsplashImageSrc =
|
||||
@ -117,10 +116,7 @@ test.describe.parallel('Image bubble block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator('img')).toHaveAttribute(
|
||||
'src',
|
||||
unsplashImageSrc
|
||||
)
|
||||
await expect(page.locator('img')).toHaveAttribute('src', unsplashImageSrc)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -3,7 +3,6 @@ import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { BubbleBlockType, defaultTextBubbleContent } from 'models'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
|
||||
test.describe('Text bubble block', () => {
|
||||
test('rich text features should work', async ({ page }) => {
|
||||
@ -51,17 +50,17 @@ test.describe('Text bubble block', () => {
|
||||
await page.getByRole('menuitem', { name: 'Create test' }).click()
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(page.locator('span.slate-bold >> nth=0')).toHaveText(
|
||||
'Bold text'
|
||||
)
|
||||
await expect(page.locator('span.slate-italic >> nth=0')).toHaveText(
|
||||
'Italic text'
|
||||
)
|
||||
await expect(page.locator('span.slate-underline >> nth=0')).toHaveText(
|
||||
'Underlined text'
|
||||
)
|
||||
await expect(
|
||||
typebotViewer(page).locator('span.slate-bold >> nth=0')
|
||||
).toHaveText('Bold text')
|
||||
await expect(
|
||||
typebotViewer(page).locator('span.slate-italic >> nth=0')
|
||||
).toHaveText('Italic text')
|
||||
await expect(
|
||||
typebotViewer(page).locator('span.slate-underline >> nth=0')
|
||||
).toHaveText('Underlined text')
|
||||
await expect(
|
||||
typebotViewer(page).locator('a[href="https://github.com"]')
|
||||
page.locator('typebot-standard').locator('a[href="https://github.com"]')
|
||||
).toHaveText('My super link')
|
||||
})
|
||||
})
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
VideoBubbleContentType,
|
||||
} from 'models'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
|
||||
const videoSrc =
|
||||
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'
|
||||
@ -57,9 +56,10 @@ test.describe.parallel('Video bubble block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator('video > source')
|
||||
).toHaveAttribute('src', videoSrc)
|
||||
await expect(page.locator('video > source').nth(1)).toHaveAttribute(
|
||||
'src',
|
||||
videoSrc
|
||||
)
|
||||
})
|
||||
|
||||
test('should display youtube video correctly', async ({ page }) => {
|
||||
@ -80,7 +80,7 @@ test.describe.parallel('Video bubble block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator('iframe')).toHaveAttribute(
|
||||
await expect(page.locator('iframe').nth(1)).toHaveAttribute(
|
||||
'src',
|
||||
'https://www.youtube.com/embed/dQw4w9WgXcQ'
|
||||
)
|
||||
@ -104,7 +104,7 @@ test.describe.parallel('Video bubble block', () => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator('iframe')).toHaveAttribute(
|
||||
await expect(page.locator('iframe').nth(1)).toHaveAttribute(
|
||||
'src',
|
||||
'https://player.vimeo.com/video/649301125'
|
||||
)
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultChoiceInputOptions, InputBlockType, ItemType } from 'models'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
test.describe.parallel('Buttons input block', () => {
|
||||
@ -42,10 +41,10 @@ test.describe.parallel('Buttons input block', () => {
|
||||
await expect(page.locator('text=Item 2')).toBeHidden()
|
||||
|
||||
await page.click('text=Preview')
|
||||
const item3Button = typebotViewer(page).locator('button >> text=Item 3')
|
||||
const item3Button = page.locator('button >> text=Item 3')
|
||||
await item3Button.click()
|
||||
await expect(item3Button).toBeHidden()
|
||||
await expect(typebotViewer(page).locator('text=Item 3')).toBeVisible()
|
||||
await expect(page.getByTestId('guest-bubble')).toHaveText('Item 3')
|
||||
await page.click('button[aria-label="Close"]')
|
||||
|
||||
await page.click('[data-testid="block2-icon"]')
|
||||
@ -64,13 +63,11 @@ test.describe.parallel('Buttons input block', () => {
|
||||
|
||||
await page.click('text=Preview')
|
||||
|
||||
await typebotViewer(page).locator('button >> text="Item 3"').click()
|
||||
await typebotViewer(page).locator('button >> text="Item 1"').click()
|
||||
await typebotViewer(page).locator('text=Go').click()
|
||||
await page.locator('button >> text="Item 3"').click()
|
||||
await page.locator('button >> text="Item 1"').click()
|
||||
await page.locator('text=Go').click()
|
||||
|
||||
await expect(
|
||||
typebotViewer(page).locator('text="Item 3, Item 1"')
|
||||
).toBeVisible()
|
||||
await expect(page.locator('text="Item 3, Item 1"')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@ -85,18 +82,18 @@ test('Variable buttons should work', async ({ page }) => {
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page).locator('text=Variable item').click()
|
||||
await expect(typebotViewer(page).locator('text=Variable item')).toBeVisible()
|
||||
await expect(typebotViewer(page).locator('text=Ok great!')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Variable item' }).click()
|
||||
await expect(page.getByTestId('guest-bubble')).toHaveText('Variable item')
|
||||
await expect(page.locator('text=Ok great!')).toBeVisible()
|
||||
await page.click('text="Item 1"')
|
||||
await page.fill('input[value="Item 1"]', '{{Item 2}}')
|
||||
await page.click('[data-testid="block1-icon"]')
|
||||
await page.click('text=Multiple choice?')
|
||||
await page.click('text="Restart"')
|
||||
await typebotViewer(page).locator('text="Variable item" >> nth=0').click()
|
||||
await typebotViewer(page).locator('text="Variable item" >> nth=1').click()
|
||||
await typebotViewer(page).locator('text="Send"').click()
|
||||
await page.getByTestId('button').first().click()
|
||||
await page.getByTestId('button').nth(1).click()
|
||||
await page.locator('text="Send"').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text="Variable item, Variable item"')
|
||||
page.locator('text="Variable item, Variable item"')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
@ -2,7 +2,6 @@ import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultDateInputOptions, InputBlockType } from 'models'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
test.describe('Date input block', () => {
|
||||
@ -21,15 +20,14 @@ test.describe('Date input block', () => {
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator('[data-testid="from-date"]')
|
||||
).toHaveAttribute('type', 'date')
|
||||
await expect(typebotViewer(page).locator(`button`)).toBeDisabled()
|
||||
await typebotViewer(page)
|
||||
.locator('[data-testid="from-date"]')
|
||||
.fill('2021-01-01')
|
||||
await typebotViewer(page).locator(`button`).click()
|
||||
await expect(typebotViewer(page).locator('text="01/01/2021"')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="from-date"]')).toHaveAttribute(
|
||||
'type',
|
||||
'date'
|
||||
)
|
||||
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled()
|
||||
await page.locator('[data-testid="from-date"]').fill('2021-01-01')
|
||||
await page.getByRole('button', { name: 'Send' }).click()
|
||||
await expect(page.locator('text="01/01/2021"')).toBeVisible()
|
||||
|
||||
await page.click(`text=Pick a date...`)
|
||||
await page.click('text=Is range?')
|
||||
@ -39,23 +37,19 @@ test.describe('Date input block', () => {
|
||||
await page.fill('#button', 'Go')
|
||||
|
||||
await page.click('text=Restart')
|
||||
await expect(page.locator(`[data-testid="from-date"]`)).toHaveAttribute(
|
||||
'type',
|
||||
'datetime-local'
|
||||
)
|
||||
await expect(page.locator(`[data-testid="to-date"]`)).toHaveAttribute(
|
||||
'type',
|
||||
'datetime-local'
|
||||
)
|
||||
await page.locator('[data-testid="from-date"]').fill('2021-01-01T11:00')
|
||||
await page.locator('[data-testid="to-date"]').fill('2022-01-01T09:00')
|
||||
await page.getByRole('button', { name: 'Go' }).click()
|
||||
await expect(
|
||||
typebotViewer(page).locator(`[data-testid="from-date"]`)
|
||||
).toHaveAttribute('type', 'datetime-local')
|
||||
await expect(
|
||||
typebotViewer(page).locator(`[data-testid="to-date"]`)
|
||||
).toHaveAttribute('type', 'datetime-local')
|
||||
await typebotViewer(page)
|
||||
.locator('[data-testid="from-date"]')
|
||||
.fill('2021-01-01T11:00')
|
||||
await typebotViewer(page)
|
||||
.locator('[data-testid="to-date"]')
|
||||
.fill('2022-01-01T09:00')
|
||||
await typebotViewer(page).locator(`button`).click()
|
||||
await expect(
|
||||
typebotViewer(page).locator(
|
||||
'text="01/01/2021, 11:00 AM to 01/01/2022, 09:00 AM"'
|
||||
)
|
||||
page.locator('text="01/01/2021, 11:00 AM to 01/01/2022, 09:00 AM"')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -2,7 +2,6 @@ import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultEmailInputOptions, InputBlockType } from 'models'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
test.describe('Email input block', () => {
|
||||
@ -22,11 +21,11 @@ test.describe('Email input block', () => {
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator(
|
||||
page.locator(
|
||||
`input[placeholder="${defaultEmailInputOptions.labels.placeholder}"]`
|
||||
)
|
||||
).toHaveAttribute('type', 'email')
|
||||
await expect(typebotViewer(page).locator(`button`)).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled()
|
||||
|
||||
await page.click(`text=${defaultEmailInputOptions.labels.placeholder}`)
|
||||
await page.fill(
|
||||
@ -41,19 +40,13 @@ test.describe('Email input block', () => {
|
||||
)
|
||||
|
||||
await page.click('text=Restart')
|
||||
await typebotViewer(page)
|
||||
.locator(`input[placeholder="Your email..."]`)
|
||||
.fill('test@test')
|
||||
await typebotViewer(page).locator('text=Go').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=Try again bro')
|
||||
).toBeVisible()
|
||||
await typebotViewer(page)
|
||||
await page.locator(`input[placeholder="Your email..."]`).fill('test@test')
|
||||
await page.getByRole('button', { name: 'Go' }).click()
|
||||
await expect(page.locator('text=Try again bro')).toBeVisible()
|
||||
await page
|
||||
.locator(`input[placeholder="Your email..."]`)
|
||||
.fill('test@test.com')
|
||||
await typebotViewer(page).locator('text=Go').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=test@test.com')
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Go' }).click()
|
||||
await expect(page.locator('text=test@test.com')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -63,7 +63,7 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
|
||||
<CodeEditor
|
||||
lang="html"
|
||||
onChange={handlePlaceholderLabelChange}
|
||||
value={options.labels.placeholder}
|
||||
defaultValue={options.labels.placeholder}
|
||||
height={'100px'}
|
||||
withVariableButton={false}
|
||||
/>
|
||||
|
@ -2,7 +2,6 @@ import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultFileInputOptions, InputBlockType } from 'models'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { freeWorkspaceId } from 'utils/playwright/databaseSetup'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
@ -24,14 +23,12 @@ test('options should work', async ({ page }) => {
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator(`text=Click to upload`)
|
||||
).toBeVisible()
|
||||
await expect(typebotViewer(page).locator(`text="Skip"`)).toBeHidden()
|
||||
await typebotViewer(page)
|
||||
await expect(page.locator(`text=Click to upload`)).toBeVisible()
|
||||
await expect(page.locator(`text="Skip"`)).toBeHidden()
|
||||
await page
|
||||
.locator(`input[type="file"]`)
|
||||
.setInputFiles([getTestAsset('avatar.jpg')])
|
||||
await expect(typebotViewer(page).locator(`text=File uploaded`)).toBeVisible()
|
||||
await expect(page.locator(`text=File uploaded`)).toBeVisible()
|
||||
await page.click('text="Collect file"')
|
||||
await page.click('text="Required?"')
|
||||
await page.click('text="Allow multiple files?"')
|
||||
@ -41,20 +38,18 @@ test('options should work', async ({ page }) => {
|
||||
await page.fill('[value="Skip"]', 'Pass')
|
||||
await page.fill('input[value="10"]', '20')
|
||||
await page.click('text="Restart"')
|
||||
await expect(typebotViewer(page).locator(`text="Pass"`)).toBeVisible()
|
||||
await expect(typebotViewer(page).locator(`text="Upload now!!"`)).toBeVisible()
|
||||
await typebotViewer(page)
|
||||
await expect(page.locator(`text="Pass"`)).toBeVisible()
|
||||
await expect(page.locator(`text="Upload now!!"`)).toBeVisible()
|
||||
await page
|
||||
.locator(`input[type="file"]`)
|
||||
.setInputFiles([
|
||||
getTestAsset('avatar.jpg'),
|
||||
getTestAsset('avatar.jpg'),
|
||||
getTestAsset('avatar.jpg'),
|
||||
])
|
||||
await expect(typebotViewer(page).locator(`text="3"`)).toBeVisible()
|
||||
await typebotViewer(page).locator('text="Go"').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator(`text="3 files uploaded"`)
|
||||
).toBeVisible()
|
||||
await expect(page.locator(`text="3"`)).toBeVisible()
|
||||
await page.locator('text="Go"').click()
|
||||
await expect(page.locator(`text="3 files uploaded"`)).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Free workspace', () => {
|
||||
|
@ -2,7 +2,6 @@ import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultNumberInputOptions, InputBlockType } from 'models'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
test.describe('Number input block', () => {
|
||||
@ -22,11 +21,11 @@ test.describe('Number input block', () => {
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator(
|
||||
page.locator(
|
||||
`input[placeholder="${defaultNumberInputOptions.labels.placeholder}"]`
|
||||
)
|
||||
).toHaveAttribute('type', 'number')
|
||||
await expect(typebotViewer(page).locator(`button`)).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled()
|
||||
|
||||
await page.click(`text=${defaultNumberInputOptions.labels.placeholder}`)
|
||||
await page.fill('#placeholder', 'Your number...')
|
||||
@ -37,15 +36,13 @@ test.describe('Number input block', () => {
|
||||
await page.fill('[role="spinbutton"] >> nth=2', '10')
|
||||
|
||||
await page.click('text=Restart')
|
||||
const input = typebotViewer(page).locator(
|
||||
`input[placeholder="Your number..."]`
|
||||
)
|
||||
const input = page.locator(`input[placeholder="Your number..."]`)
|
||||
await input.fill('-1')
|
||||
await input.press('Enter')
|
||||
await input.fill('150')
|
||||
await input.press('Enter')
|
||||
await input.fill('50')
|
||||
await input.press('Enter')
|
||||
await expect(typebotViewer(page).locator('text=50')).toBeVisible()
|
||||
await expect(page.locator('text=50')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -3,7 +3,6 @@ import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultPaymentInputOptions, InputBlockType } from 'models'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { stripePaymentForm } from '@/test/utils/selectorUtils'
|
||||
|
||||
test.describe('Payment input block', () => {
|
||||
@ -59,9 +58,9 @@ test.describe('Payment input block', () => {
|
||||
.locator(`[placeholder="MM / YY"]`)
|
||||
.fill('12 / 25')
|
||||
await stripePaymentForm(page).locator(`[placeholder="CVC"]`).fill('240')
|
||||
await typebotViewer(page).locator(`text="Pay 30€"`).click()
|
||||
await page.locator(`text="Pay 30€"`).click()
|
||||
await expect(
|
||||
typebotViewer(page).locator(`text="Your card has been declined."`)
|
||||
page.locator(`text="Your card has been declined."`)
|
||||
).toBeVisible()
|
||||
await stripePaymentForm(page)
|
||||
.locator(`[placeholder="1234 1234 1234 1234"]`)
|
||||
@ -69,7 +68,7 @@ test.describe('Payment input block', () => {
|
||||
const zipInput = stripePaymentForm(page).getByPlaceholder('90210')
|
||||
const isZipInputVisible = await zipInput.isVisible()
|
||||
if (isZipInputVisible) await zipInput.fill('12345')
|
||||
await typebotViewer(page).locator(`text="Pay 30€"`).click()
|
||||
await expect(typebotViewer(page).locator(`text="Success"`)).toBeVisible()
|
||||
await page.locator(`text="Pay 30€"`).click()
|
||||
await expect(page.locator(`text="Success"`)).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -2,7 +2,6 @@ import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultPhoneInputOptions, InputBlockType } from 'models'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
test.describe('Phone input block', () => {
|
||||
@ -22,11 +21,11 @@ test.describe('Phone input block', () => {
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator(
|
||||
page.locator(
|
||||
`input[placeholder="${defaultPhoneInputOptions.labels.placeholder}"]`
|
||||
)
|
||||
).toHaveAttribute('type', 'tel')
|
||||
await expect(typebotViewer(page).locator(`button`)).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled()
|
||||
|
||||
await page.click(`text=${defaultPhoneInputOptions.labels.placeholder}`)
|
||||
await page.fill('#placeholder', '+33 XX XX XX XX')
|
||||
@ -37,21 +36,14 @@ test.describe('Phone input block', () => {
|
||||
)
|
||||
|
||||
await page.click('text=Restart')
|
||||
await typebotViewer(page)
|
||||
.locator(`input[placeholder="+33 XX XX XX XX"]`)
|
||||
.fill('+33 6 73')
|
||||
await expect(typebotViewer(page).locator(`img`)).toHaveAttribute(
|
||||
'alt',
|
||||
'France'
|
||||
)
|
||||
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=Try again bro')
|
||||
).toBeVisible()
|
||||
await typebotViewer(page)
|
||||
await page.locator(`input[placeholder="+33 XX XX XX XX"]`).type('+33 6 73')
|
||||
await expect(page.getByRole('combobox')).toHaveText(/🇫🇷.+/)
|
||||
await page.locator('button >> text="Go"').click()
|
||||
await expect(page.locator('text=Try again bro')).toBeVisible()
|
||||
await page
|
||||
.locator(`input[placeholder="+33 XX XX XX XX"]`)
|
||||
.fill('+33 6 73 54 45 67')
|
||||
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||
await expect(typebotViewer(page).locator('text=+33673544567')).toBeVisible()
|
||||
await page.locator('button >> text="Go"').click()
|
||||
await expect(page.locator('text=+33 6 73 54 45 67')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -2,7 +2,6 @@ import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultRatingInputOptions, InputBlockType } from 'models'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
const boxSvg = `<svg
|
||||
@ -33,10 +32,10 @@ test('options should work', async ({ page }) => {
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator(`text=Send`)).toBeHidden()
|
||||
await typebotViewer(page).locator(`text=8`).click()
|
||||
await typebotViewer(page).locator(`text=Send`).click()
|
||||
await expect(typebotViewer(page).locator(`text=8`)).toBeVisible()
|
||||
await expect(page.locator(`text=Send`)).toBeHidden()
|
||||
await page.locator(`text=8`).click()
|
||||
await page.locator(`text=Send`).click()
|
||||
await expect(page.locator(`text=8`)).toBeVisible()
|
||||
await page.click('text=Rate from 0 to 10')
|
||||
await page.click('text="10"')
|
||||
await page.click('text="5"')
|
||||
@ -48,14 +47,10 @@ test('options should work', async ({ page }) => {
|
||||
await page.fill('[placeholder="Not likely at all"]', 'Not likely at all')
|
||||
await page.fill('[placeholder="Extremely likely"]', 'Extremely likely')
|
||||
await page.click('text="Restart"')
|
||||
await expect(typebotViewer(page).locator(`text=8`)).toBeHidden()
|
||||
await expect(typebotViewer(page).locator(`text=4`)).toBeHidden()
|
||||
await expect(
|
||||
typebotViewer(page).locator(`text=Not likely at all`)
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
typebotViewer(page).locator(`text=Extremely likely`)
|
||||
).toBeVisible()
|
||||
await typebotViewer(page).locator(`svg >> nth=4`).click()
|
||||
await expect(typebotViewer(page).locator(`text=5`)).toBeVisible()
|
||||
await expect(page.locator(`text=8`)).toBeHidden()
|
||||
await expect(page.locator(`text=4`)).toBeHidden()
|
||||
await expect(page.locator(`text=Not likely at all`)).toBeVisible()
|
||||
await expect(page.locator(`text=Extremely likely`)).toBeVisible()
|
||||
await page.locator(`svg >> nth=4`).click()
|
||||
await expect(page.locator(`text=5`)).toBeVisible()
|
||||
})
|
||||
|
@ -2,7 +2,6 @@ import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultTextInputOptions, InputBlockType } from 'models'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
test.describe.parallel('Text input block', () => {
|
||||
@ -22,11 +21,11 @@ test.describe.parallel('Text input block', () => {
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator(
|
||||
page.locator(
|
||||
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
||||
)
|
||||
).toHaveAttribute('type', 'text')
|
||||
await expect(typebotViewer(page).locator(`button`)).toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Send' })).toBeDisabled()
|
||||
|
||||
await page.click(`text=${defaultTextInputOptions.labels.placeholder}`)
|
||||
await page.fill('#placeholder', 'Your name...')
|
||||
@ -35,8 +34,8 @@ test.describe.parallel('Text input block', () => {
|
||||
|
||||
await page.click('text=Restart')
|
||||
await expect(
|
||||
typebotViewer(page).locator(`textarea[placeholder="Your name..."]`)
|
||||
page.locator(`textarea[placeholder="Your name..."]`)
|
||||
).toBeVisible()
|
||||
await expect(typebotViewer(page).locator(`text=Go`)).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -2,7 +2,6 @@ import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from 'utils/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from 'utils/playwright/databaseHelpers'
|
||||
import { defaultUrlInputOptions, InputBlockType } from 'models'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
test.describe('Url input block', () => {
|
||||
@ -22,11 +21,13 @@ test.describe('Url input block', () => {
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(
|
||||
typebotViewer(page).locator(
|
||||
page.locator(
|
||||
`input[placeholder="${defaultUrlInputOptions.labels.placeholder}"]`
|
||||
)
|
||||
).toHaveAttribute('type', 'url')
|
||||
await expect(typebotViewer(page).locator(`button`)).toBeDisabled()
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator(`button`)
|
||||
).toBeDisabled()
|
||||
|
||||
await page.click(`text=${defaultUrlInputOptions.labels.placeholder}`)
|
||||
await page.fill('#placeholder', 'Your URL...')
|
||||
@ -38,19 +39,15 @@ test.describe('Url input block', () => {
|
||||
)
|
||||
|
||||
await page.click('text=Restart')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator(`input[placeholder="Your URL..."]`)
|
||||
.fill('https://https://test')
|
||||
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=Try again bro')
|
||||
).toBeVisible()
|
||||
await typebotViewer(page)
|
||||
await page.locator('button >> text="Go"').click()
|
||||
await expect(page.locator('text=Try again bro')).toBeVisible()
|
||||
await page
|
||||
.locator(`input[placeholder="Your URL..."]`)
|
||||
.fill('https://website.com')
|
||||
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=https://website.com')
|
||||
).toBeVisible()
|
||||
await page.locator('button >> text="Go"').click()
|
||||
await expect(page.locator('text=https://website.com')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -36,7 +36,7 @@ test.describe('Chatwoot block', () => {
|
||||
await page.getByLabel('Phone number').fill('+33654347543')
|
||||
await page.getByRole('button', { name: 'Preview', exact: true }).click()
|
||||
await expect(
|
||||
page.getByText("Chatwoot won't open in preview mode").nth(0)
|
||||
page.getByText('Chatwoot block is not supported in preview').nth(0)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,5 @@
|
||||
import test, { expect, Page } from '@playwright/test'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
@ -33,25 +32,17 @@ test.describe.parallel('Google sheets integration', () => {
|
||||
)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
.fill('georges@gmail.com')
|
||||
await Promise.all([
|
||||
page.waitForResponse(
|
||||
(resp) =>
|
||||
resp
|
||||
.request()
|
||||
.url()
|
||||
.includes(
|
||||
'/api/integrations/google-sheets/spreadsheets/1k_pIDw3YHl9tlZusbBVSBRY0PeRPd2H6t4Nj7rwnOtM/sheets/0'
|
||||
) &&
|
||||
resp.status() === 200 &&
|
||||
resp.request().method() === 'POST'
|
||||
),
|
||||
typebotViewer(page)
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
.press('Enter'),
|
||||
])
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
.press('Enter')
|
||||
await expect(
|
||||
page.getByText('Succesfully inserted row in CRM > Sheet1').nth(0)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Update row should work', async ({ page }) => {
|
||||
@ -82,25 +73,17 @@ test.describe.parallel('Google sheets integration', () => {
|
||||
)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
.fill('test@test.com')
|
||||
await Promise.all([
|
||||
page.waitForResponse(
|
||||
(resp) =>
|
||||
resp
|
||||
.request()
|
||||
.url()
|
||||
.includes(
|
||||
'/api/integrations/google-sheets/spreadsheets/1k_pIDw3YHl9tlZusbBVSBRY0PeRPd2H6t4Nj7rwnOtM/sheets/0'
|
||||
) &&
|
||||
resp.status() === 200 &&
|
||||
resp.request().method() === 'POST'
|
||||
),
|
||||
typebotViewer(page)
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
.press('Enter'),
|
||||
])
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
.press('Enter')
|
||||
await expect(
|
||||
page.getByText('Succesfully updated row in CRM > Sheet1').nth(0)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Get row should work', async ({ page }) => {
|
||||
@ -143,15 +126,17 @@ test.describe.parallel('Google sheets integration', () => {
|
||||
await createNewVar(page, 'Last name')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
.fill('test2@test.com')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type your email..."]')
|
||||
.press('Enter')
|
||||
await expect(typebotViewer(page).locator('text=Your name is:')).toHaveText(
|
||||
/John|Fred|Georges/
|
||||
)
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text=Your name is:')
|
||||
).toHaveText(/John|Fred|Georges/)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -181,7 +181,7 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||
</Flex>
|
||||
{options.isBodyCode ? (
|
||||
<CodeEditor
|
||||
value={options.body ?? ''}
|
||||
defaultValue={options.body ?? ''}
|
||||
onChange={handleBodyChange}
|
||||
lang="html"
|
||||
/>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
@ -64,7 +63,7 @@ test.describe('Send email block', () => {
|
||||
await page.fill('[data-testid="body-input"]', 'Here is my email')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page).locator('text=Go').click()
|
||||
await page.locator('typebot-standard').locator('text=Go').click()
|
||||
await expect(
|
||||
page.locator('text=Emails are not sent in preview mode >> nth=0')
|
||||
).toBeVisible()
|
||||
|
@ -223,7 +223,7 @@ export const WebhookSettings = ({
|
||||
/>
|
||||
{(options.isCustomBody ?? true) && (
|
||||
<CodeEditor
|
||||
value={localWebhook.body ?? ''}
|
||||
defaultValue={localWebhook.body ?? ''}
|
||||
lang="json"
|
||||
onChange={handleBodyChange}
|
||||
debounceTimeout={0}
|
||||
@ -262,7 +262,7 @@ export const WebhookSettings = ({
|
||||
</Button>
|
||||
)}
|
||||
{testResponse && (
|
||||
<CodeEditor isReadOnly lang="json" value={testResponse} />
|
||||
<CodeEditor isReadOnly lang="json" defaultValue={testResponse} />
|
||||
)}
|
||||
{(testResponse || options?.responseVariableMapping.length > 0) && (
|
||||
<Accordion allowMultiple>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
@ -51,30 +50,33 @@ test.describe('Condition block', () => {
|
||||
await page.fill('input[placeholder="Type a value..."]', '20')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type a number..."]')
|
||||
.fill('15')
|
||||
await typebotViewer(page).locator('text=Send').click()
|
||||
await page.locator('typebot-standard').locator('text=Send').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=You are younger than 20')
|
||||
page.locator('typebot-standard').getByText('You are younger than 20')
|
||||
).toBeVisible()
|
||||
|
||||
await page.click('text=Restart')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type a number..."]')
|
||||
.fill('45')
|
||||
await typebotViewer(page).locator('text=Send').click()
|
||||
await page.locator('typebot-standard').locator('text=Send').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=You are older than 20')
|
||||
page.locator('typebot-standard').getByText('You are older than 20')
|
||||
).toBeVisible()
|
||||
|
||||
await page.click('text=Restart')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type a number..."]')
|
||||
.fill('90')
|
||||
await typebotViewer(page).locator('text=Send').click()
|
||||
await page.locator('typebot-standard').locator('text=Send').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=You are older than 80')
|
||||
page.locator('typebot-standard').getByText('You are older than 80')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,4 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
@ -20,7 +19,7 @@ test.describe('Redirect block', () => {
|
||||
await page.fill('input[placeholder="Type a URL..."]', 'google.com')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page).locator('text=Go to URL').click()
|
||||
await page.locator('typebot-standard').locator('text=Go to URL').click()
|
||||
await expect(page).toHaveURL('https://www.google.com')
|
||||
await page.goBack()
|
||||
|
||||
@ -30,7 +29,7 @@ test.describe('Redirect block', () => {
|
||||
await page.click('text=Preview')
|
||||
const [newPage] = await Promise.all([
|
||||
context.waitForEvent('page'),
|
||||
typebotViewer(page).locator('text=Go to URL').click(),
|
||||
page.locator('typebot-standard').locator('text=Go to URL').click(),
|
||||
])
|
||||
await newPage.waitForLoadState()
|
||||
await expect(newPage).toHaveURL('https://www.google.com')
|
||||
|
@ -41,8 +41,8 @@ export const ScriptSettings = ({ options, onOptionsChange }: Props) => {
|
||||
<Stack>
|
||||
<Text>Code:</Text>
|
||||
<CodeEditor
|
||||
value={options.content ?? ''}
|
||||
lang="js"
|
||||
defaultValue={options.content ?? ''}
|
||||
lang="javascript"
|
||||
onChange={handleCodeChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
@ -20,7 +19,7 @@ test.describe('Script block', () => {
|
||||
)
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page).locator('text=Trigger code').click()
|
||||
await page.getByRole('button', { name: 'Trigger code' }).click()
|
||||
await expect(page).toHaveURL('https://www.google.com')
|
||||
})
|
||||
})
|
||||
|
@ -51,9 +51,9 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
|
||||
|
||||
{options.isCode ?? false ? (
|
||||
<CodeEditor
|
||||
value={options.expressionToEvaluate ?? ''}
|
||||
defaultValue={options.expressionToEvaluate ?? ''}
|
||||
onChange={handleExpressionChange}
|
||||
lang="js"
|
||||
lang="javascript"
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
|
@ -1,5 +1,4 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
@ -42,18 +41,19 @@ test.describe('Set variable block', () => {
|
||||
await page.fill('textarea', '1000 + {{Total}}')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page)
|
||||
await page
|
||||
.locator('typebot-standard')
|
||||
.locator('input[placeholder="Type a number..."]')
|
||||
.fill('365')
|
||||
await typebotViewer(page).locator('text=Send').click()
|
||||
await page.locator('typebot-standard').locator('text=Send').click()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=Multiplication: 365000')
|
||||
page.locator('typebot-standard').locator('text=Multiplication: 365000')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=Custom var: Custom value')
|
||||
page.locator('typebot-standard').locator('text=Custom var: Custom value')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
typebotViewer(page).locator('text=Addition: 366000')
|
||||
page.locator('typebot-standard').locator('text=Addition: 366000')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,4 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
@ -35,7 +34,9 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.click('text=Group #2')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator('text=Second block')).toBeVisible()
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text=Second block')
|
||||
).toBeVisible()
|
||||
|
||||
await page.click('[aria-label="Close"]')
|
||||
await page.click('text=Jump to Group #2 in My link typebot 2')
|
||||
@ -44,9 +45,11 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.click('button >> text=Start')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page).locator('input').fill('Hello there!')
|
||||
await typebotViewer(page).locator('input').press('Enter')
|
||||
await expect(typebotViewer(page).locator('text=Hello there!')).toBeVisible()
|
||||
await page.locator('typebot-standard').locator('input').fill('Hello there!')
|
||||
await page.locator('typebot-standard').locator('input').press('Enter')
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text=Hello there!')
|
||||
).toBeVisible()
|
||||
|
||||
await page.click('[aria-label="Close"]')
|
||||
await page.click('text=Jump to Start in My link typebot 2')
|
||||
@ -61,5 +64,7 @@ test('should be configurable', async ({ page }) => {
|
||||
await page.click('button >> text=Hello')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator('text=Hello world')).toBeVisible()
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text=Hello world')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
@ -1,5 +1,4 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
@ -17,10 +16,14 @@ test.describe('Wait block', () => {
|
||||
await page.getByRole('textbox', { name: 'Seconds to wait for:' }).fill('3')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page).locator('text=Wait now').click()
|
||||
await page.getByRole('button', { name: 'Wait now' }).click()
|
||||
await page.waitForTimeout(1000)
|
||||
await expect(typebotViewer(page).locator('text="Hi there!"')).toBeHidden()
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text="Hi there!"')
|
||||
).toBeHidden()
|
||||
await page.waitForTimeout(3000)
|
||||
await expect(typebotViewer(page).locator('text="Hi there!"')).toBeVisible()
|
||||
await expect(
|
||||
page.locator('typebot-standard').locator('text="Hi there!"')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -50,6 +50,7 @@ export const parseNewTypebot = ({
|
||||
return {
|
||||
folderId,
|
||||
name,
|
||||
version: '3',
|
||||
workspaceId,
|
||||
groups: [startGroup],
|
||||
edges: [],
|
||||
|
@ -1,64 +1,49 @@
|
||||
import {
|
||||
chakra,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { chakra, useColorModeValue } from '@chakra-ui/react'
|
||||
import { Popup } from '@typebot.io/react'
|
||||
import { useUser } from '@/features/account'
|
||||
import { AnswerInput, Typebot } from 'models'
|
||||
import { Typebot } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { getViewerUrl, sendRequest } from 'utils'
|
||||
import { sendRequest } from 'utils'
|
||||
import confetti from 'canvas-confetti'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { parseTypebotToPublicTypebot } from '@/features/publish'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
type Props = { totalTypebots: number }
|
||||
|
||||
export const OnboardingModal = ({ totalTypebots }: Props) => {
|
||||
const { push } = useRouter()
|
||||
const botPath = useColorModeValue(
|
||||
'/bots/onboarding.json',
|
||||
'/bots/onboarding-dark.json'
|
||||
)
|
||||
const { user, updateUser } = useUser()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [typebot, setTypebot] = useState<Typebot>()
|
||||
const confettiCanvaContainer = useRef<HTMLCanvasElement | null>(null)
|
||||
const confettiCanon = useRef<confetti.CreateTypes>()
|
||||
const [chosenCategories, setChosenCategories] = useState<string[]>([])
|
||||
const [openedOnce, setOpenedOnce] = useState(false)
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplate()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
const isNewUser =
|
||||
user &&
|
||||
new Date(user?.createdAt as unknown as string).toDateString() ===
|
||||
new Date().toDateString() &&
|
||||
totalTypebots === 0
|
||||
|
||||
useEffect(() => {
|
||||
if (openedOnce) return
|
||||
const isNewUser =
|
||||
user &&
|
||||
new Date(user?.createdAt as unknown as string).toDateString() ===
|
||||
new Date().toDateString() &&
|
||||
totalTypebots === 0
|
||||
if (isNewUser) {
|
||||
onOpen()
|
||||
setOpenedOnce(true)
|
||||
const fetchTemplate = async () => {
|
||||
const { data, error } = await sendRequest(botPath)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
setTypebot(data as Typebot)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user])
|
||||
|
||||
fetchTemplate()
|
||||
}, [botPath, showToast])
|
||||
|
||||
useEffect(() => {
|
||||
initConfettis()
|
||||
return () => {
|
||||
window.removeEventListener('message', handleIncomingMessage)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [confettiCanvaContainer.current])
|
||||
}, [])
|
||||
|
||||
const initConfettis = () => {
|
||||
if (!confettiCanvaContainer.current || confettiCanon.current) return
|
||||
@ -66,48 +51,41 @@ export const OnboardingModal = ({ totalTypebots }: Props) => {
|
||||
resize: true,
|
||||
useWorker: true,
|
||||
})
|
||||
window.addEventListener('message', handleIncomingMessage)
|
||||
}
|
||||
|
||||
const handleIncomingMessage = (message: MessageEvent) => {
|
||||
if (message.data.from === 'typebot') {
|
||||
if (message.data.action === 'shootConfettis' && confettiCanon.current)
|
||||
shootConfettis(confettiCanon.current)
|
||||
}
|
||||
const handleBotEnd = () => {
|
||||
setTimeout(() => {
|
||||
push('/typebots/create', { query: { isFirstBot: true } })
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const fetchTemplate = async () => {
|
||||
const { data, error } = await sendRequest(botPath)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
setTypebot(data as Typebot)
|
||||
}
|
||||
|
||||
const handleNewAnswer = async (answer: AnswerInput) => {
|
||||
const isName = answer.variableId === 'cl126f4hf000i2e6d8zvzc3t1'
|
||||
const isCompany = answer.variableId === 'cl126jqww000w2e6dq9yv4ifq'
|
||||
const isCategories = answer.variableId === 'cl126mo3t001b2e6dvyi16bkd'
|
||||
const isOtherCategories = answer.variableId === 'cl126q38p001q2e6d0hj23f6b'
|
||||
if (isName) updateUser({ name: answer.content })
|
||||
if (isCompany) updateUser({ company: answer.content })
|
||||
const handleNewAnswer = async (answer: {
|
||||
message: string
|
||||
blockId: string
|
||||
}) => {
|
||||
const isName = answer.blockId === 'cl126820m000g2e6dfleq78bt'
|
||||
const isCompany = answer.blockId === 'cl126jioz000v2e6dwrk1f2cb'
|
||||
const isCategories = answer.blockId === 'cl126lb8v00142e6duv5qe08l'
|
||||
const isOtherCategories = answer.blockId === 'cl126pv7n001o2e6dajltc4qz'
|
||||
const answeredAllQuestions =
|
||||
isOtherCategories || (isCategories && !answer.message.includes('Other'))
|
||||
if (answeredAllQuestions && confettiCanon.current)
|
||||
shootConfettis(confettiCanon.current)
|
||||
if (isName) updateUser({ name: answer.message })
|
||||
if (isCompany) updateUser({ company: answer.message })
|
||||
if (isCategories) {
|
||||
const onboardingCategories = answer.content.split(', ')
|
||||
const onboardingCategories = answer.message.split(', ')
|
||||
updateUser({ onboardingCategories })
|
||||
setChosenCategories(onboardingCategories)
|
||||
}
|
||||
if (isOtherCategories)
|
||||
updateUser({
|
||||
onboardingCategories: [...chosenCategories, answer.content],
|
||||
onboardingCategories: [...chosenCategories, answer.message],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="3xl"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
blockScrollOnMount={false}
|
||||
>
|
||||
<>
|
||||
<chakra.canvas
|
||||
ref={confettiCanvaContainer}
|
||||
pos="fixed"
|
||||
@ -118,23 +96,18 @@ export const OnboardingModal = ({ totalTypebots }: Props) => {
|
||||
zIndex={9999}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<ModalOverlay />
|
||||
<ModalContent h="85vh">
|
||||
<ModalBody p="10">
|
||||
{typebot && (
|
||||
<TypebotViewer
|
||||
apiHost={getViewerUrl()}
|
||||
typebot={parseTypebotToPublicTypebot(typebot)}
|
||||
predefinedVariables={{
|
||||
Name: user?.name?.split(' ')[0] ?? undefined,
|
||||
}}
|
||||
onNewAnswer={handleNewAnswer}
|
||||
style={{ borderRadius: '0.25rem' }}
|
||||
/>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{typebot && (
|
||||
<Popup
|
||||
typebot={typebot}
|
||||
prefilledVariables={{
|
||||
Name: user?.name?.split(' ')[0] ?? undefined,
|
||||
}}
|
||||
defaultOpen={isNewUser}
|
||||
onAnswer={handleNewAnswer}
|
||||
onEnd={handleBotEnd}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -56,6 +56,7 @@ const duplicateTypebot = (
|
||||
return {
|
||||
typebot: {
|
||||
...typebot,
|
||||
version: '3',
|
||||
id,
|
||||
name: `${typebot.name} copy`,
|
||||
publicId: null,
|
||||
|
@ -11,32 +11,25 @@ import {
|
||||
UseToastOptions,
|
||||
VStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { useEditor } from '../providers/EditorProvider'
|
||||
import { useGraph } from '@/features/graph'
|
||||
import { useTypebot } from '../providers/TypebotProvider'
|
||||
import { Log } from 'db'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { getViewerUrl } from 'utils'
|
||||
import React, { useState } from 'react'
|
||||
import { headerHeight } from '../constants'
|
||||
import { parseTypebotToPublicTypebot } from '@/features/publish'
|
||||
import { Standard } from '@typebot.io/react'
|
||||
import { ChatReply } from 'models'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
|
||||
export const PreviewDrawer = () => {
|
||||
const isDark = useColorMode().colorMode === 'dark'
|
||||
const { typebot } = useTypebot()
|
||||
const { typebot, save, isSavingLoading } = useTypebot()
|
||||
const { setRightPanel, startPreviewAtGroup } = useEditor()
|
||||
const { setPreviewingEdge } = useGraph()
|
||||
const { setPreviewingBlock } = useGraph()
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [width, setWidth] = useState(500)
|
||||
const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false)
|
||||
const [restartKey, setRestartKey] = useState(0)
|
||||
|
||||
const publicTypebot = useMemo(
|
||||
() => (typebot ? { ...parseTypebotToPublicTypebot(typebot) } : undefined),
|
||||
[typebot]
|
||||
)
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
const handleMouseDown = () => {
|
||||
@ -54,15 +47,19 @@ export const PreviewDrawer = () => {
|
||||
}
|
||||
useEventListener('mouseup', handleMouseUp)
|
||||
|
||||
const handleRestartClick = () => setRestartKey((key) => key + 1)
|
||||
const handleRestartClick = async () => {
|
||||
await save()
|
||||
setRestartKey((key) => key + 1)
|
||||
}
|
||||
|
||||
const handleCloseClick = () => {
|
||||
setPreviewingEdge(undefined)
|
||||
setPreviewingBlock(undefined)
|
||||
setRightPanel(undefined)
|
||||
}
|
||||
|
||||
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>
|
||||
showToast(log as UseToastOptions)
|
||||
const handleNewLogs = (logs: ChatReply['logs']) => {
|
||||
logs?.forEach((log) => showToast(log as UseToastOptions))
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -92,29 +89,25 @@ export const PreviewDrawer = () => {
|
||||
|
||||
<VStack w="full" spacing={4}>
|
||||
<Flex justifyContent={'space-between'} w="full">
|
||||
<Button onClick={handleRestartClick}>Restart</Button>
|
||||
<Button onClick={handleRestartClick} isLoading={isSavingLoading}>
|
||||
Restart
|
||||
</Button>
|
||||
<CloseButton onClick={handleCloseClick} />
|
||||
</Flex>
|
||||
|
||||
{publicTypebot && (
|
||||
<Flex
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'lg'}
|
||||
h="full"
|
||||
w="full"
|
||||
{typebot && (
|
||||
<Standard
|
||||
key={restartKey + (startPreviewAtGroup ?? '')}
|
||||
pointerEvents={isResizing ? 'none' : 'auto'}
|
||||
>
|
||||
<TypebotViewer
|
||||
apiHost={getViewerUrl()}
|
||||
typebot={publicTypebot}
|
||||
onNewGroupVisible={setPreviewingEdge}
|
||||
onNewLog={handleNewLog}
|
||||
startGroupId={startPreviewAtGroup}
|
||||
isPreview
|
||||
style={{ borderRadius: '10px' }}
|
||||
/>
|
||||
</Flex>
|
||||
typebot={typebot}
|
||||
startGroupId={startPreviewAtGroup}
|
||||
onNewInputBlock={setPreviewingBlock}
|
||||
onNewLogs={handleNewLogs}
|
||||
style={{
|
||||
borderWidth: '1px',
|
||||
borderRadius: '0.25rem',
|
||||
pointerEvents: isResizing ? 'none' : 'auto',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
</Flex>
|
||||
|
@ -20,7 +20,7 @@ import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { isDefined, isNotDefined } from 'utils'
|
||||
import { EditableTypebotName } from './EditableTypebotName'
|
||||
import { getBubbleActions } from 'typebot-js'
|
||||
import { open as openSupportBubble } from '@typebot.io/js'
|
||||
import Link from 'next/link'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
import { headerHeight } from '../../constants'
|
||||
@ -70,7 +70,7 @@ export const TypebotHeader = () => {
|
||||
|
||||
const handleHelpClick = () => {
|
||||
isCloudProdInstance
|
||||
? getBubbleActions().open()
|
||||
? openSupportBubble()
|
||||
: window.open('https://docs.typebot.io', '_blank')
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
importTypebotInDatabase,
|
||||
} from 'utils/playwright/databaseActions'
|
||||
import {
|
||||
typebotViewer,
|
||||
waitForSuccessfulDeleteRequest,
|
||||
waitForSuccessfulPostRequest,
|
||||
waitForSuccessfulPutRequest,
|
||||
@ -200,16 +199,16 @@ test('Preview from group should work', async ({ page }) => {
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('[aria-label="Preview bot from this group"] >> nth=1')
|
||||
await expect(
|
||||
typebotViewer(page).locator('text="Hello this is group 1"')
|
||||
page.locator('typebot-standard').locator('text="Hello this is group 1"')
|
||||
).toBeVisible()
|
||||
await page.click('[aria-label="Preview bot from this group"] >> nth=2')
|
||||
await expect(
|
||||
typebotViewer(page).locator('text="Hello this is group 2"')
|
||||
page.locator('typebot-standard').locator('text="Hello this is group 2"')
|
||||
).toBeVisible()
|
||||
await page.click('[aria-label="Close"]')
|
||||
await page.click('text="Preview"')
|
||||
await expect(
|
||||
typebotViewer(page).locator('text="Hello this is group 1"')
|
||||
page.locator('typebot-standard').locator('text="Hello this is group 1"')
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { DashboardFolder, WorkspaceRole } from 'db'
|
||||
import { env } from 'utils'
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
@ -11,7 +10,6 @@ import {
|
||||
Wrap,
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebotDnd } from '../TypebotDndProvider'
|
||||
import { useUser } from '@/features/account'
|
||||
import React, { useState } from 'react'
|
||||
import { BackButton } from './BackButton'
|
||||
import { OnboardingModal } from '../../dashboard/components/OnboardingModal'
|
||||
@ -26,14 +24,12 @@ import { CreateFolderButton } from './CreateFolderButton'
|
||||
import { ButtonSkeleton, FolderButton } from './FolderButton'
|
||||
import { TypebotButton } from './TypebotButton'
|
||||
import { TypebotCardOverlay } from './TypebotButtonOverlay'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
|
||||
type Props = { folder: DashboardFolder | null }
|
||||
|
||||
const dragDistanceTolerance = 20
|
||||
|
||||
export const FolderContent = ({ folder }: Props) => {
|
||||
const { user } = useUser()
|
||||
const { workspace, currentRole } = useWorkspace()
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
|
||||
const {
|
||||
@ -160,14 +156,7 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
|
||||
return (
|
||||
<Flex w="full" flex="1" justify="center">
|
||||
{typebots &&
|
||||
!isTypebotLoading &&
|
||||
user &&
|
||||
isCloudProdInstance &&
|
||||
folder === null &&
|
||||
env('E2E_TEST') !== 'true' && (
|
||||
<OnboardingModal totalTypebots={typebots.length} />
|
||||
)}
|
||||
{typebots && <OnboardingModal totalTypebots={typebots.length} />}
|
||||
<Stack w="1000px" spacing={6}>
|
||||
<Skeleton isLoaded={folder?.name !== undefined}>
|
||||
<Heading as="h1">{folder?.name}</Heading>
|
||||
|
@ -64,6 +64,7 @@ export const BlockNode = ({
|
||||
setFocusedGroupId,
|
||||
previewingEdge,
|
||||
isReadOnly,
|
||||
previewingBlock,
|
||||
} = useGraph()
|
||||
const { mouseOverBlock, setMouseOverBlock } = useBlockDnd()
|
||||
const { typebot, updateBlock } = useTypebot()
|
||||
@ -76,7 +77,10 @@ export const BlockNode = ({
|
||||
)
|
||||
const blockRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const isPreviewing = isConnecting || previewingEdge?.to.blockId === block.id
|
||||
const isPreviewing =
|
||||
isConnecting ||
|
||||
previewingEdge?.to.blockId === block.id ||
|
||||
previewingBlock?.id === block.id
|
||||
|
||||
const onDrag = (position: NodePosition) => {
|
||||
if (block.type === 'start' || !onMouseDown) return
|
||||
|
@ -61,6 +61,7 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
connectingIds,
|
||||
setConnectingIds,
|
||||
previewingEdge,
|
||||
previewingBlock,
|
||||
isReadOnly,
|
||||
focusedGroupId,
|
||||
setFocusedGroupId,
|
||||
@ -91,6 +92,7 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
}, [group.title])
|
||||
|
||||
const isPreviewing =
|
||||
previewingBlock?.groupId === group.id ||
|
||||
previewingEdge?.from.groupId === group.id ||
|
||||
(previewingEdge?.to.groupId === group.id &&
|
||||
isNotDefined(previewingEdge.to.blockId))
|
||||
|
@ -62,11 +62,18 @@ export type Endpoint = {
|
||||
|
||||
export type GroupsCoordinates = IdMap<Coordinates>
|
||||
|
||||
type PreviewingBlock = {
|
||||
id: string
|
||||
groupId: string
|
||||
}
|
||||
|
||||
const graphContext = createContext<{
|
||||
graphPosition: Position
|
||||
setGraphPosition: Dispatch<SetStateAction<Position>>
|
||||
connectingIds: ConnectingIds | null
|
||||
setConnectingIds: Dispatch<SetStateAction<ConnectingIds | null>>
|
||||
previewingBlock?: PreviewingBlock
|
||||
setPreviewingBlock: Dispatch<SetStateAction<PreviewingBlock | undefined>>
|
||||
previewingEdge?: Edge
|
||||
setPreviewingEdge: Dispatch<SetStateAction<Edge | undefined>>
|
||||
sourceEndpoints: IdMap<Endpoint>
|
||||
@ -99,6 +106,7 @@ export const GraphProvider = ({
|
||||
)
|
||||
const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null)
|
||||
const [previewingEdge, setPreviewingEdge] = useState<Edge>()
|
||||
const [previewingBlock, setPreviewingBlock] = useState<PreviewingBlock>()
|
||||
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [openedBlockId, setOpenedBlockId] = useState<string>()
|
||||
@ -139,6 +147,8 @@ export const GraphProvider = ({
|
||||
isReadOnly,
|
||||
focusedGroupId,
|
||||
setFocusedGroupId,
|
||||
setPreviewingBlock,
|
||||
previewingBlock,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
MenuItem,
|
||||
useDisclosure,
|
||||
ButtonProps,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
@ -27,6 +28,7 @@ import { ChangePlanModal, isFreePlan, LimitReached } from '@/features/billing'
|
||||
import { timeSince } from '@/utils/helpers'
|
||||
|
||||
export const PublishButton = (props: ButtonProps) => {
|
||||
const warningTextColor = useColorModeValue('red.300', 'red.600')
|
||||
const { workspace } = useWorkspace()
|
||||
const { push, query } = useRouter()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
@ -71,12 +73,17 @@ export const PublishButton = (props: ButtonProps) => {
|
||||
type={LimitReached.FILE_INPUT}
|
||||
/>
|
||||
<Tooltip
|
||||
borderRadius="md"
|
||||
hasArrow
|
||||
placement="bottom-end"
|
||||
label={
|
||||
<Stack>
|
||||
<Text>There are non published changes.</Text>
|
||||
{!publishedTypebot?.version ? (
|
||||
<Text color={warningTextColor} fontWeight="semibold">
|
||||
This will deploy your bot with an updated engine. Make sure to
|
||||
test it properly in preview mode before publishing.
|
||||
</Text>
|
||||
) : (
|
||||
<Text>There are non published changes.</Text>
|
||||
)}
|
||||
<Text fontStyle="italic">
|
||||
Published version from{' '}
|
||||
{publishedTypebot &&
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
IframeModal,
|
||||
WixModal,
|
||||
} from './modals'
|
||||
import { OtherModal } from './modals/OtherModal'
|
||||
|
||||
export type ModalProps = {
|
||||
publicId: string
|
||||
@ -139,7 +140,7 @@ export const integrationsList = [
|
||||
<EmbedButton
|
||||
logo={<OtherLogo height={100} width="70px" />}
|
||||
label="Other"
|
||||
Modal={JavascriptModal}
|
||||
Modal={OtherModal}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { AlertInfo } from '@/components/AlertInfo'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
HStack,
|
||||
IconButton,
|
||||
Heading,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
ModalFooter,
|
||||
} from '@chakra-ui/react'
|
||||
import { capitalize } from 'utils'
|
||||
import { EmbedTypeMenu } from './EmbedTypeMenu/EmbedTypeMenu'
|
||||
|
||||
type Props = {
|
||||
selectedEmbedType: 'standard' | 'popup' | 'bubble' | undefined
|
||||
titlePrefix: string
|
||||
isOpen: boolean
|
||||
isPublished: boolean
|
||||
children: React.ReactNode
|
||||
onClose: () => void
|
||||
onSelectEmbedType: (type: 'standard' | 'popup' | 'bubble' | undefined) => void
|
||||
}
|
||||
|
||||
export const EmbedModal = ({
|
||||
selectedEmbedType,
|
||||
isOpen,
|
||||
isPublished,
|
||||
titlePrefix,
|
||||
children,
|
||||
onSelectEmbedType,
|
||||
onClose,
|
||||
}: Props) => (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size={!selectedEmbedType ? '2xl' : 'xl'}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
{selectedEmbedType && (
|
||||
<IconButton
|
||||
icon={<ChevronLeftIcon />}
|
||||
aria-label="back"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
mr={2}
|
||||
onClick={() => onSelectEmbedType(undefined)}
|
||||
/>
|
||||
)}
|
||||
<Heading size="md">
|
||||
{titlePrefix}{' '}
|
||||
{selectedEmbedType && `- ${capitalize(selectedEmbedType)}`}
|
||||
</Heading>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing={4} pt={0}>
|
||||
{!isPublished && (
|
||||
<AlertInfo>You need to publish your bot first.</AlertInfo>
|
||||
)}
|
||||
{!selectedEmbedType ? (
|
||||
<EmbedTypeMenu onSelectEmbedType={onSelectEmbedType} />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
@ -0,0 +1,33 @@
|
||||
import { MotionStack } from '@/components/MotionStack'
|
||||
import { Stack, Button, StackProps, Text, ButtonProps } from '@chakra-ui/react'
|
||||
import { BubbleIllustration } from './illustrations/BubbleIllustration'
|
||||
|
||||
type Props = StackProps & Pick<ButtonProps, 'isDisabled'>
|
||||
|
||||
export const BubbleMenuButton = (props: Props) => {
|
||||
return (
|
||||
<MotionStack
|
||||
as={Button}
|
||||
fontWeight="normal"
|
||||
alignItems="center"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
whiteSpace={'normal'}
|
||||
spacing="6"
|
||||
flex="1"
|
||||
height="250px"
|
||||
animate="default"
|
||||
whileHover="animateBubbles"
|
||||
transition={{ staggerChildren: 0.1 }}
|
||||
{...props}
|
||||
>
|
||||
<BubbleIllustration />
|
||||
<Stack>
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Bubble
|
||||
</Text>
|
||||
<Text textColor="gray.500">Embed in a chat bubble</Text>
|
||||
</Stack>
|
||||
</MotionStack>
|
||||
)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { HStack } from '@chakra-ui/react'
|
||||
import { BubbleMenuButton } from './BubbleMenuButton'
|
||||
import { PopupMenuButton } from './PopupMenuButton'
|
||||
import { StandardMenuButton } from './StandardMenuButton'
|
||||
|
||||
type Props = {
|
||||
onSelectEmbedType: (type: 'standard' | 'popup' | 'bubble') => void
|
||||
}
|
||||
|
||||
export const EmbedTypeMenu = ({ onSelectEmbedType }: Props) => {
|
||||
return (
|
||||
<HStack spacing={4}>
|
||||
<StandardMenuButton onClick={() => onSelectEmbedType('standard')} />
|
||||
<PopupMenuButton onClick={() => onSelectEmbedType('popup')} />
|
||||
<BubbleMenuButton onClick={() => onSelectEmbedType('bubble')} />
|
||||
</HStack>
|
||||
)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { MotionStack } from '@/components/MotionStack'
|
||||
import { Stack, Button, StackProps, Text, ButtonProps } from '@chakra-ui/react'
|
||||
import { PopupIllustration } from './illustrations/PopupIllustration'
|
||||
|
||||
type Props = StackProps & Pick<ButtonProps, 'isDisabled'>
|
||||
|
||||
export const PopupMenuButton = (props: Props) => {
|
||||
return (
|
||||
<MotionStack
|
||||
as={Button}
|
||||
fontWeight="normal"
|
||||
alignItems="center"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
whiteSpace={'normal'}
|
||||
spacing="6"
|
||||
height="250px"
|
||||
flex="1"
|
||||
animate="default"
|
||||
whileHover="animateBubbles"
|
||||
transition={{ staggerChildren: 0.1 }}
|
||||
{...props}
|
||||
>
|
||||
<PopupIllustration />
|
||||
<Stack>
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Popup
|
||||
</Text>
|
||||
<Text textColor="gray.500">
|
||||
Embed in a popup on top of your website
|
||||
</Text>
|
||||
</Stack>
|
||||
</MotionStack>
|
||||
)
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { MotionStack } from '@/components/MotionStack'
|
||||
import { Stack, Button, StackProps, Text, ButtonProps } from '@chakra-ui/react'
|
||||
import { StandardIllustration } from './illustrations/StandardIllustration'
|
||||
|
||||
type Props = StackProps & Pick<ButtonProps, 'isDisabled'>
|
||||
|
||||
export const StandardMenuButton = (props: Props) => {
|
||||
return (
|
||||
<MotionStack
|
||||
as={Button}
|
||||
fontWeight="normal"
|
||||
alignItems="center"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
whiteSpace={'normal'}
|
||||
spacing="6"
|
||||
height="250px"
|
||||
flex="1"
|
||||
animate="default"
|
||||
whileHover="animateBubbles"
|
||||
transition={{ staggerChildren: 0.1 }}
|
||||
{...props}
|
||||
>
|
||||
<StandardIllustration />
|
||||
<Stack>
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Standard
|
||||
</Text>
|
||||
<Text textColor="gray.500">Embed in a container on your site</Text>
|
||||
</Stack>
|
||||
</MotionStack>
|
||||
)
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import { colors } from '@/lib/theme'
|
||||
import { useColorModeValue } from '@chakra-ui/react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { animationVariants } from './animationVariants'
|
||||
|
||||
export const BubbleIllustration = () => {
|
||||
const bubbleColor = useColorModeValue('white', colors.blue[100])
|
||||
return (
|
||||
<svg
|
||||
width="100"
|
||||
viewBox="0 0 500 500"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="500"
|
||||
height="500"
|
||||
rx="20"
|
||||
fill={useColorModeValue(colors.gray['200'], colors.gray['900'])}
|
||||
/>
|
||||
<rect x="164" y="59" width="287" height="305" rx="10" fill="#0042DA" />
|
||||
<motion.rect
|
||||
x="227"
|
||||
y="91"
|
||||
width="156"
|
||||
height="34"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<motion.rect
|
||||
x="227"
|
||||
y="134"
|
||||
width="156"
|
||||
height="65"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
|
||||
<motion.circle
|
||||
cx="198"
|
||||
cy="228"
|
||||
r="20"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<motion.rect
|
||||
x="227"
|
||||
y="208"
|
||||
width="156"
|
||||
height="40"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="412"
|
||||
cy="277"
|
||||
r="20"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<motion.rect
|
||||
x="253"
|
||||
y="257"
|
||||
width="130"
|
||||
height="40"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
|
||||
<circle cx="411" cy="430" r="40" fill="#0042DA" />
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import { colors } from '@/lib/theme'
|
||||
import { useColorModeValue } from '@chakra-ui/react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { animationVariants } from './animationVariants'
|
||||
|
||||
export const PopupIllustration = () => {
|
||||
const bubbleColor = useColorModeValue('white', colors.blue[100])
|
||||
return (
|
||||
<svg
|
||||
width="100"
|
||||
viewBox="0 0 500 500"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="500"
|
||||
height="500"
|
||||
rx="20"
|
||||
fill={useColorModeValue(colors.gray['400'], colors.gray['900'])}
|
||||
/>
|
||||
<rect x="105" y="77" width="290" height="352" rx="10" fill="#0042DA" />
|
||||
<motion.rect
|
||||
x="171"
|
||||
y="117"
|
||||
width="156"
|
||||
height="34"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<motion.rect
|
||||
x="171"
|
||||
y="160"
|
||||
width="156"
|
||||
height="65"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
|
||||
<motion.circle
|
||||
cx="142"
|
||||
cy="254"
|
||||
r="20"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<motion.rect
|
||||
x="171"
|
||||
y="234"
|
||||
width="156"
|
||||
height="40"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="356"
|
||||
cy="303"
|
||||
r="20"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<motion.rect
|
||||
x="197"
|
||||
y="283"
|
||||
width="130"
|
||||
height="40"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import { colors } from '@/lib/theme'
|
||||
import { useColorModeValue } from '@chakra-ui/react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { animationVariants } from './animationVariants'
|
||||
|
||||
export const StandardIllustration = () => {
|
||||
const gray = useColorModeValue(colors.gray[400], colors.gray[700])
|
||||
const bubbleColor = useColorModeValue('white', colors.blue[100])
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 500 500"
|
||||
width="100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="500"
|
||||
height="500"
|
||||
rx="20"
|
||||
fill={useColorModeValue(colors.gray['200'], colors.gray['900'])}
|
||||
/>
|
||||
<rect x="49" y="49" width="108" height="109" rx="10" fill={gray} />
|
||||
<rect x="188" y="74" width="263" height="25" rx="5" fill={gray} />
|
||||
<rect x="188" y="111" width="263" height="25" rx="5" fill={gray} />
|
||||
<rect x="49" y="189" width="402" height="262" rx="10" fill="#0042DA" />
|
||||
|
||||
<motion.rect
|
||||
variants={animationVariants}
|
||||
x="121"
|
||||
y="217"
|
||||
width="218"
|
||||
height="34"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
/>
|
||||
<motion.rect
|
||||
variants={animationVariants}
|
||||
x="121"
|
||||
y="260"
|
||||
width="218"
|
||||
height="65"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
/>
|
||||
<motion.circle
|
||||
variants={animationVariants}
|
||||
cx="93"
|
||||
cy="354"
|
||||
r="20"
|
||||
fill={bubbleColor}
|
||||
/>
|
||||
<motion.rect
|
||||
variants={animationVariants}
|
||||
x="121"
|
||||
y="334"
|
||||
width="218"
|
||||
height="40"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
/>
|
||||
<motion.circle
|
||||
variants={animationVariants}
|
||||
cx="407"
|
||||
cy="410"
|
||||
r="20"
|
||||
fill={bubbleColor}
|
||||
/>
|
||||
<motion.rect
|
||||
variants={animationVariants}
|
||||
x="250"
|
||||
y="390"
|
||||
width="130"
|
||||
height="40"
|
||||
rx="10"
|
||||
fill={bubbleColor}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { Variants } from 'framer-motion'
|
||||
|
||||
export const animationVariants: Variants = {
|
||||
animateBubbles: {
|
||||
opacity: [0, 1],
|
||||
},
|
||||
default: { opacity: 1 },
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import prettier from 'prettier/standalone'
|
||||
import parserHtml from 'prettier/parser-html'
|
||||
import { BubbleParams } from 'typebot-js'
|
||||
import { parseInitBubbleCode, typebotJsHtml } from '../params'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
import { FlexProps } from '@chakra-ui/react'
|
||||
|
||||
type ChatEmbedCodeProps = {
|
||||
withStarterVariables?: boolean
|
||||
onCopied?: () => void
|
||||
} & Pick<BubbleParams, 'button' | 'proactiveMessage'>
|
||||
|
||||
export const ChatEmbedCode = ({
|
||||
proactiveMessage,
|
||||
button,
|
||||
}: ChatEmbedCodeProps & FlexProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
const snippet = prettier.format(
|
||||
createSnippet({
|
||||
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
|
||||
typebot?.publicId
|
||||
}`,
|
||||
button,
|
||||
proactiveMessage,
|
||||
}),
|
||||
{
|
||||
parser: 'html',
|
||||
plugins: [parserHtml],
|
||||
}
|
||||
)
|
||||
return <CodeEditor value={snippet} lang="html" isReadOnly />
|
||||
}
|
||||
|
||||
const createSnippet = (params: BubbleParams): string => {
|
||||
const jsCode = parseInitBubbleCode(params)
|
||||
return `${typebotJsHtml}
|
||||
<script>${jsCode}</script>`
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
import {
|
||||
StackProps,
|
||||
Stack,
|
||||
Heading,
|
||||
HStack,
|
||||
Input,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
Switch,
|
||||
Text,
|
||||
Image,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { useUser } from '@/features/account'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { BubbleParams } from 'typebot-js'
|
||||
import { ColorPicker } from '@/components/ColorPicker'
|
||||
|
||||
type ChatEmbedSettingsProps = {
|
||||
onUpdateSettings: (
|
||||
windowSettings: Pick<BubbleParams, 'button' | 'proactiveMessage'>
|
||||
) => void
|
||||
}
|
||||
export const ChatEmbedSettings = ({
|
||||
onUpdateSettings,
|
||||
...props
|
||||
}: ChatEmbedSettingsProps & StackProps) => {
|
||||
const { user } = useUser()
|
||||
const { typebot } = useTypebot()
|
||||
const [proactiveMessageChecked, setProactiveMessageChecked] = useState(false)
|
||||
const [isCustomIconChecked, setIsCustomIconChecked] = useState(false)
|
||||
|
||||
const [rememberProMessageChecked] = useState(true)
|
||||
const [customIconInputValue, setCustomIconInputValue] = useState('')
|
||||
|
||||
const [inputValues, setInputValues] = useState({
|
||||
messageDelay: '0',
|
||||
messageContent: 'I have a question for you!',
|
||||
avatarUrl: typebot?.theme.chat.hostAvatar?.url ?? user?.image ?? '',
|
||||
})
|
||||
|
||||
const [bubbleColor, setBubbleColor] = useState(
|
||||
typebot?.theme.chat.buttons.backgroundColor ?? '#0042DA'
|
||||
)
|
||||
|
||||
const [bubbleIconColor, setIconBubbleColor] = useState(
|
||||
typebot?.theme.chat.buttons.color ?? '#FFFFFF'
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (proactiveMessageChecked) {
|
||||
onUpdateSettings({
|
||||
button: {
|
||||
color: bubbleColor,
|
||||
iconUrl: isCustomIconChecked ? customIconInputValue : undefined,
|
||||
iconColor:
|
||||
bubbleIconColor === '#FFFFFF' ? undefined : bubbleIconColor,
|
||||
},
|
||||
proactiveMessage: {
|
||||
delay: parseInt(inputValues.messageDelay) * 1000,
|
||||
textContent: inputValues.messageContent,
|
||||
avatarUrl: inputValues.avatarUrl,
|
||||
rememberClose: rememberProMessageChecked,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
onUpdateSettings({
|
||||
button: {
|
||||
color: bubbleColor,
|
||||
iconUrl: isCustomIconChecked ? customIconInputValue : undefined,
|
||||
iconColor:
|
||||
bubbleIconColor === '#FFFFFF' ? undefined : bubbleIconColor,
|
||||
},
|
||||
proactiveMessage: undefined,
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
inputValues,
|
||||
bubbleColor,
|
||||
rememberProMessageChecked,
|
||||
customIconInputValue,
|
||||
bubbleIconColor,
|
||||
proactiveMessageChecked,
|
||||
isCustomIconChecked,
|
||||
])
|
||||
|
||||
return (
|
||||
<Stack {...props} spacing="4">
|
||||
<Heading fontSize="md" fontWeight="semibold">
|
||||
Chat bubble settings
|
||||
</Heading>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Button color</Text>
|
||||
<ColorPicker
|
||||
initialColor={bubbleColor}
|
||||
onColorChange={setBubbleColor}
|
||||
/>
|
||||
</Flex>
|
||||
<HStack justify="space-between">
|
||||
<Text>Icon color</Text>
|
||||
<ColorPicker
|
||||
initialColor={bubbleIconColor}
|
||||
onColorChange={setIconBubbleColor}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack justifyContent="space-between">
|
||||
<FormLabel htmlFor="custom-icon" mb="0" flexShrink={0}>
|
||||
Custom button icon?
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="custom-icon"
|
||||
onChange={() => setIsCustomIconChecked(!isCustomIconChecked)}
|
||||
isChecked={isCustomIconChecked}
|
||||
/>
|
||||
</HStack>
|
||||
{isCustomIconChecked && (
|
||||
<>
|
||||
<HStack pl="4">
|
||||
<Text>Url:</Text>
|
||||
<Input
|
||||
placeholder={'Paste image link (.png, .svg)'}
|
||||
value={customIconInputValue}
|
||||
onChange={(e) => setCustomIconInputValue(e.target.value)}
|
||||
minW="0"
|
||||
/>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
<Flex alignItems="center">
|
||||
<FormControl
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
w="full"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<FormLabel htmlFor="fullscreen-option" mb="0">
|
||||
Enable popup message?
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="fullscreen-option"
|
||||
onChange={() =>
|
||||
setProactiveMessageChecked(!proactiveMessageChecked)
|
||||
}
|
||||
isChecked={proactiveMessageChecked}
|
||||
/>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
{proactiveMessageChecked && (
|
||||
<>
|
||||
<Flex pl="4">
|
||||
<HStack
|
||||
bgColor="white"
|
||||
shadow="md"
|
||||
rounded="md"
|
||||
p="3"
|
||||
maxW="280px"
|
||||
spacing={4}
|
||||
>
|
||||
{inputValues.avatarUrl && (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<Image src={inputValues.avatarUrl} w="40px" rounded="full" />
|
||||
)}
|
||||
<Text>{inputValues.messageContent}</Text>
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center" pl="4">
|
||||
<Text>Appearance delay</Text>
|
||||
<NumberInput
|
||||
onChange={(messageDelay) =>
|
||||
setInputValues({
|
||||
...inputValues,
|
||||
messageDelay,
|
||||
})
|
||||
}
|
||||
value={inputValues.messageDelay}
|
||||
min={0}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center" pl="4">
|
||||
<Text>Avatar URL</Text>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) =>
|
||||
setInputValues({
|
||||
...inputValues,
|
||||
avatarUrl: e.target.value,
|
||||
})
|
||||
}
|
||||
value={inputValues.avatarUrl}
|
||||
placeholder={'Paste image link (.png, .jpg)'}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center" pl="4">
|
||||
<Text>Message content</Text>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) =>
|
||||
setInputValues({
|
||||
...inputValues,
|
||||
messageContent: e.target.value,
|
||||
})
|
||||
}
|
||||
value={inputValues.messageContent}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import { FlexProps } from '@chakra-ui/react'
|
||||
import parserHtml from 'prettier/parser-html'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { parseInitContainerCode, typebotJsHtml } from '../params'
|
||||
import { IframeParams } from 'typebot-js'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
|
||||
type ContainerEmbedCodeProps = {
|
||||
widthLabel: string
|
||||
heightLabel: string
|
||||
withStarterVariables?: boolean
|
||||
onCopied?: () => void
|
||||
}
|
||||
|
||||
export const ContainerEmbedCode = ({
|
||||
widthLabel,
|
||||
heightLabel,
|
||||
}: ContainerEmbedCodeProps & FlexProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
const snippet = prettier.format(
|
||||
parseSnippet({
|
||||
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
|
||||
typebot?.publicId
|
||||
}`,
|
||||
heightLabel,
|
||||
widthLabel,
|
||||
}),
|
||||
{
|
||||
parser: 'html',
|
||||
plugins: [parserHtml],
|
||||
}
|
||||
)
|
||||
|
||||
return <CodeEditor value={snippet} lang="html" isReadOnly />
|
||||
}
|
||||
|
||||
type SnippetProps = IframeParams &
|
||||
Pick<ContainerEmbedCodeProps, 'widthLabel' | 'heightLabel'>
|
||||
|
||||
const parseSnippet = ({
|
||||
url,
|
||||
customDomain,
|
||||
backgroundColor,
|
||||
hiddenVariables,
|
||||
...embedProps
|
||||
}: SnippetProps): string => {
|
||||
const jsCode = parseInitContainerCode({
|
||||
customDomain,
|
||||
hiddenVariables,
|
||||
backgroundColor,
|
||||
url,
|
||||
})
|
||||
return `${typebotJsHtml}
|
||||
<div id="typebot-container" style="width: ${embedProps.widthLabel}; height: ${embedProps.heightLabel};"></div>
|
||||
<script>${jsCode}</script>`
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
import {
|
||||
StackProps,
|
||||
Stack,
|
||||
Flex,
|
||||
Heading,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Switch,
|
||||
Input,
|
||||
HStack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { DropdownList } from '@/components/DropdownList'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
type StandardEmbedWindowSettingsProps = {
|
||||
onUpdateWindowSettings: (windowSettings: {
|
||||
heightLabel: string
|
||||
widthLabel: string
|
||||
}) => void
|
||||
}
|
||||
export const StandardEmbedWindowSettings = ({
|
||||
onUpdateWindowSettings,
|
||||
...props
|
||||
}: StandardEmbedWindowSettingsProps & StackProps) => {
|
||||
const [fullscreen, setFullscreen] = useState(false)
|
||||
const [inputValues, setInputValues] = useState({
|
||||
widthValue: '100',
|
||||
widthType: '%',
|
||||
heightValue: '600',
|
||||
heightType: 'px',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
onUpdateWindowSettings({
|
||||
widthLabel: fullscreen
|
||||
? '100%'
|
||||
: inputValues.widthValue + inputValues.widthType,
|
||||
heightLabel: fullscreen
|
||||
? '100vh'
|
||||
: inputValues.heightValue + inputValues.heightType,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputValues, fullscreen])
|
||||
|
||||
const handleWidthTypeSelect = (widthType: string) =>
|
||||
setInputValues({ ...inputValues, widthType })
|
||||
const handleHeightTypeSelect = (heightType: string) =>
|
||||
setInputValues({ ...inputValues, heightType })
|
||||
|
||||
return (
|
||||
<Stack {...props}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Heading fontSize="md" fontWeight="semibold" style={{ flexShrink: 0 }}>
|
||||
Window settings
|
||||
</Heading>
|
||||
<FormControl
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
w="full"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<FormLabel htmlFor="fullscreen-option" mb="1">
|
||||
Set to fullscreen?
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="fullscreen-option"
|
||||
onChange={() => setFullscreen(!fullscreen)}
|
||||
isChecked={fullscreen}
|
||||
/>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
|
||||
{!fullscreen && (
|
||||
<>
|
||||
<Flex justify="space-between" align="center" mb="2">
|
||||
<Text>Width</Text>
|
||||
<HStack>
|
||||
<Input
|
||||
onChange={(e) =>
|
||||
setInputValues({
|
||||
...inputValues,
|
||||
widthValue: e.target.value,
|
||||
})
|
||||
}
|
||||
w="70px"
|
||||
value={inputValues.widthValue}
|
||||
/>
|
||||
<DropdownList<string>
|
||||
items={['px', '%']}
|
||||
onItemSelect={handleWidthTypeSelect}
|
||||
currentItem={inputValues.widthType}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center" mb="2">
|
||||
<Text>Height</Text>
|
||||
<HStack>
|
||||
<Input
|
||||
onChange={(e) =>
|
||||
setInputValues({
|
||||
...inputValues,
|
||||
heightValue: e.target.value,
|
||||
})
|
||||
}
|
||||
w="70px"
|
||||
value={inputValues.heightValue}
|
||||
/>
|
||||
<DropdownList<string>
|
||||
items={['px', '%']}
|
||||
onItemSelect={handleHeightTypeSelect}
|
||||
currentItem={inputValues.heightType}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import { FlexProps } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import parserHtml from 'prettier/parser-html'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { PopupParams } from 'typebot-js'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
import { parseInitPopupCode, typebotJsHtml } from '../params'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
|
||||
type PopupEmbedCodeProps = {
|
||||
delay?: number
|
||||
withStarterVariables?: boolean
|
||||
onCopied?: () => void
|
||||
}
|
||||
|
||||
export const PopupEmbedCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const snippet = prettier.format(
|
||||
createSnippet({
|
||||
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
|
||||
typebot?.publicId
|
||||
}`,
|
||||
delay,
|
||||
}),
|
||||
{
|
||||
parser: 'html',
|
||||
plugins: [parserHtml],
|
||||
}
|
||||
)
|
||||
return <CodeEditor value={snippet} lang="html" isReadOnly />
|
||||
}
|
||||
|
||||
const createSnippet = (params: PopupParams): string => {
|
||||
const jsCode = parseInitPopupCode(params)
|
||||
return `${typebotJsHtml}
|
||||
<script>${jsCode}</script>`
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import {
|
||||
StackProps,
|
||||
Stack,
|
||||
Flex,
|
||||
Heading,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
Switch,
|
||||
HStack,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PopupParams } from 'typebot-js'
|
||||
|
||||
type PopupEmbedSettingsProps = {
|
||||
onUpdateSettings: (windowSettings: Pick<PopupParams, 'delay'>) => void
|
||||
}
|
||||
export const PopupEmbedSettings = ({
|
||||
onUpdateSettings,
|
||||
...props
|
||||
}: PopupEmbedSettingsProps & StackProps) => {
|
||||
const [isEnabled, setIsEnabled] = useState(false)
|
||||
const [inputValue, setInputValue] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
onUpdateSettings({
|
||||
delay: isEnabled ? inputValue * 1000 : undefined,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputValue, isEnabled])
|
||||
|
||||
return (
|
||||
<Stack {...props}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Heading fontSize="md" fontWeight="semibold">
|
||||
Popup settings
|
||||
</Heading>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="space-between" align="center" mb="2">
|
||||
<HStack>
|
||||
<p>Appearance delay</p>
|
||||
<Switch
|
||||
isChecked={isEnabled}
|
||||
onChange={(e) => setIsEnabled(e.target.checked)}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
{isEnabled && (
|
||||
<NumberInput
|
||||
onChange={(_, val) => setInputValue(val)}
|
||||
value={inputValue}
|
||||
min={0}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberIncrementStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberIncrementStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
import { FlexProps } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { BubbleParams, IframeParams, PopupParams } from 'typebot-js'
|
||||
import {
|
||||
parseInitBubbleCode,
|
||||
parseInitContainerCode,
|
||||
parseInitPopupCode,
|
||||
} from './params'
|
||||
import parserBabel from 'prettier/parser-babel'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
|
||||
type StandardReactDivProps = { widthLabel: string; heightLabel: string }
|
||||
export const StandardReactDiv = ({
|
||||
widthLabel,
|
||||
heightLabel,
|
||||
}: StandardReactDivProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const snippet = prettier.format(
|
||||
parseContainerSnippet({
|
||||
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
|
||||
typebot?.publicId
|
||||
}`,
|
||||
heightLabel,
|
||||
widthLabel,
|
||||
}),
|
||||
{
|
||||
parser: 'babel',
|
||||
plugins: [parserBabel],
|
||||
}
|
||||
)
|
||||
return <CodeEditor value={snippet} lang="js" isReadOnly />
|
||||
}
|
||||
|
||||
type SnippetProps = IframeParams &
|
||||
Pick<StandardReactDivProps, 'widthLabel' | 'heightLabel'>
|
||||
|
||||
const parseContainerSnippet = ({
|
||||
url,
|
||||
customDomain,
|
||||
backgroundColor,
|
||||
hiddenVariables,
|
||||
...embedProps
|
||||
}: SnippetProps): string => {
|
||||
const jsCode = parseInitContainerCode({
|
||||
url,
|
||||
customDomain,
|
||||
backgroundColor,
|
||||
hiddenVariables,
|
||||
})
|
||||
return `import Typebot from "typebot-js";
|
||||
|
||||
const Component = () => {
|
||||
useEffect(()=> {
|
||||
${jsCode}
|
||||
}, [])
|
||||
|
||||
return <div id="typebot-container" style={{width: "${embedProps.widthLabel}", height: "${embedProps.heightLabel}"}} />
|
||||
}`
|
||||
}
|
||||
|
||||
type PopupEmbedCodeProps = {
|
||||
delay?: number
|
||||
withStarterVariables?: boolean
|
||||
}
|
||||
|
||||
export const PopupReactCode = ({ delay }: PopupEmbedCodeProps & FlexProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const snippet = prettier.format(
|
||||
parsePopupSnippet({
|
||||
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
|
||||
typebot?.publicId
|
||||
}`,
|
||||
delay,
|
||||
}),
|
||||
{
|
||||
parser: 'babel',
|
||||
plugins: [parserBabel],
|
||||
}
|
||||
)
|
||||
return <CodeEditor value={snippet} lang="js" isReadOnly />
|
||||
}
|
||||
|
||||
const parsePopupSnippet = ({
|
||||
url,
|
||||
customDomain,
|
||||
backgroundColor,
|
||||
hiddenVariables,
|
||||
delay,
|
||||
}: PopupParams): string => {
|
||||
const jsCode = parseInitPopupCode({
|
||||
url,
|
||||
customDomain,
|
||||
backgroundColor,
|
||||
hiddenVariables,
|
||||
delay,
|
||||
})
|
||||
return `import Typebot from "typebot-js";
|
||||
|
||||
const Component = () => {
|
||||
useEffect(()=> {
|
||||
${jsCode}
|
||||
}, [])
|
||||
|
||||
return <></>;
|
||||
}`
|
||||
}
|
||||
|
||||
type ChatEmbedCodeProps = {
|
||||
withStarterVariables?: boolean
|
||||
} & Pick<BubbleParams, 'button' | 'proactiveMessage'>
|
||||
|
||||
export const ChatReactCode = ({
|
||||
proactiveMessage,
|
||||
button,
|
||||
}: ChatEmbedCodeProps & FlexProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const snippet = prettier.format(
|
||||
parseBubbleSnippet({
|
||||
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
|
||||
typebot?.publicId
|
||||
}`,
|
||||
button,
|
||||
proactiveMessage,
|
||||
}),
|
||||
{
|
||||
parser: 'babel',
|
||||
plugins: [parserBabel],
|
||||
}
|
||||
)
|
||||
return <CodeEditor value={snippet} lang="js" isReadOnly />
|
||||
}
|
||||
|
||||
const parseBubbleSnippet = ({
|
||||
url,
|
||||
customDomain,
|
||||
backgroundColor,
|
||||
hiddenVariables,
|
||||
proactiveMessage,
|
||||
button,
|
||||
}: BubbleParams): string => {
|
||||
const jsCode = parseInitBubbleCode({
|
||||
url,
|
||||
customDomain,
|
||||
backgroundColor,
|
||||
hiddenVariables,
|
||||
proactiveMessage,
|
||||
button,
|
||||
})
|
||||
return `import Typebot from "typebot-js";
|
||||
|
||||
const Component = () => {
|
||||
useEffect(()=> {
|
||||
${jsCode}
|
||||
}, [])
|
||||
|
||||
return <></>
|
||||
}`
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
import {
|
||||
BubbleParams,
|
||||
ButtonParams,
|
||||
IframeParams,
|
||||
PopupParams,
|
||||
ProactiveMessageParams,
|
||||
} from 'typebot-js'
|
||||
import parserBabel from 'prettier/parser-babel'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
const parseStringParam = (fieldName: string, fieldValue?: string) =>
|
||||
fieldValue ? `${fieldName}: "${fieldValue}",` : ``
|
||||
|
||||
const parseNonStringParam = (
|
||||
fieldName: string,
|
||||
fieldValue?: number | boolean
|
||||
) => (isDefined(fieldValue) ? `${fieldName}: ${fieldValue},` : ``)
|
||||
|
||||
const parseCustomDomain = (domain?: string): string =>
|
||||
parseStringParam('customDomain', domain)
|
||||
|
||||
const parseHiddenVariables = (
|
||||
variables: { [key: string]: string | undefined } | undefined
|
||||
): string => (variables ? `hiddenVariables: ${JSON.stringify(variables)},` : ``)
|
||||
|
||||
const parseBackgroundColor = (bgColor?: string): string =>
|
||||
parseStringParam('backgroundColor', bgColor)
|
||||
|
||||
const parseDelay = (delay?: number) => parseNonStringParam('delay', delay)
|
||||
|
||||
const parseButton = (button?: ButtonParams): string => {
|
||||
if (!button) return ''
|
||||
const iconUrlString = parseStringParam('iconUrl', button.iconUrl)
|
||||
const buttonColorstring = parseStringParam('color', button.color)
|
||||
const buttonIconColorString = parseStringParam('iconColor', button.iconColor)
|
||||
return `button: {${iconUrlString}${buttonColorstring}${buttonIconColorString}},`
|
||||
}
|
||||
|
||||
const parseProactiveMessage = (
|
||||
proactiveMessage?: ProactiveMessageParams
|
||||
): string => {
|
||||
if (!proactiveMessage) return ``
|
||||
const { avatarUrl, textContent, delay } = proactiveMessage
|
||||
const avatarUrlString = parseStringParam('avatarUrl', avatarUrl)
|
||||
const textContentString = parseStringParam('textContent', textContent)
|
||||
const delayString = parseNonStringParam('delay', delay)
|
||||
return `proactiveMessage: {${avatarUrlString}${textContentString}${delayString}},`
|
||||
}
|
||||
|
||||
const parseIframeParams = ({
|
||||
customDomain,
|
||||
hiddenVariables,
|
||||
backgroundColor,
|
||||
}: Pick<
|
||||
IframeParams,
|
||||
'customDomain' | 'hiddenVariables' | 'backgroundColor'
|
||||
>) => ({
|
||||
customDomainString: parseCustomDomain(customDomain),
|
||||
hiddenVariablesString: parseHiddenVariables(hiddenVariables),
|
||||
bgColorString: parseBackgroundColor(backgroundColor),
|
||||
})
|
||||
|
||||
const parsePopupParams = ({ delay }: Pick<PopupParams, 'delay'>) => ({
|
||||
delayString: parseDelay(delay),
|
||||
})
|
||||
|
||||
const parseBubbleParams = ({
|
||||
button,
|
||||
proactiveMessage,
|
||||
}: Pick<BubbleParams, 'button' | 'proactiveMessage'>) => ({
|
||||
proactiveMessageString: parseProactiveMessage(proactiveMessage),
|
||||
buttonString: parseButton(button),
|
||||
})
|
||||
|
||||
export const parseInitContainerCode = ({
|
||||
url,
|
||||
customDomain,
|
||||
backgroundColor,
|
||||
hiddenVariables,
|
||||
}: IframeParams) => {
|
||||
const { customDomainString, hiddenVariablesString, bgColorString } =
|
||||
parseIframeParams({
|
||||
customDomain,
|
||||
hiddenVariables,
|
||||
backgroundColor,
|
||||
})
|
||||
return prettier.format(
|
||||
`Typebot.initContainer("typebot-container", {
|
||||
url: "${url}",${bgColorString}${customDomainString}${hiddenVariablesString}
|
||||
});`,
|
||||
{ parser: 'babel', plugins: [parserBabel] }
|
||||
)
|
||||
}
|
||||
|
||||
export const parseInitPopupCode = ({
|
||||
url,
|
||||
customDomain,
|
||||
hiddenVariables,
|
||||
backgroundColor,
|
||||
delay,
|
||||
}: PopupParams) => {
|
||||
const { customDomainString, hiddenVariablesString, bgColorString } =
|
||||
parseIframeParams({
|
||||
customDomain,
|
||||
hiddenVariables,
|
||||
backgroundColor,
|
||||
})
|
||||
const { delayString } = parsePopupParams({ delay })
|
||||
return prettier.format(
|
||||
`var typebotCommands = Typebot.initPopup({url: "${url}",${delayString}${bgColorString}${customDomainString}${hiddenVariablesString}});`,
|
||||
{ parser: 'babel', plugins: [parserBabel] }
|
||||
)
|
||||
}
|
||||
|
||||
export const parseInitBubbleCode = ({
|
||||
url,
|
||||
customDomain,
|
||||
hiddenVariables,
|
||||
backgroundColor,
|
||||
button,
|
||||
proactiveMessage,
|
||||
}: BubbleParams) => {
|
||||
const { customDomainString, hiddenVariablesString, bgColorString } =
|
||||
parseIframeParams({
|
||||
customDomain,
|
||||
hiddenVariables,
|
||||
backgroundColor,
|
||||
})
|
||||
const { buttonString, proactiveMessageString } = parseBubbleParams({
|
||||
button,
|
||||
proactiveMessage,
|
||||
})
|
||||
return prettier.format(
|
||||
`var typebotCommands = Typebot.initBubble({url: "${url}",${bgColorString}${customDomainString}${hiddenVariablesString}${proactiveMessageString}${buttonString}});`,
|
||||
{ parser: 'babel', plugins: [parserBabel] }
|
||||
)
|
||||
}
|
||||
|
||||
export const typebotJsHtml = `<script src="https://unpkg.com/typebot-js@2.2"></script>`
|
@ -1,155 +0,0 @@
|
||||
import { HStack, Button, Text, Stack } from '@chakra-ui/react'
|
||||
|
||||
type ChooseEmbedTypeListProps = {
|
||||
onSelectEmbedType: (type: 'standard' | 'popup' | 'bubble') => void
|
||||
disabledTypes?: ('standard' | 'popup' | 'bubble')[]
|
||||
}
|
||||
|
||||
export const ChooseEmbedTypeList = ({
|
||||
onSelectEmbedType,
|
||||
disabledTypes = [],
|
||||
}: ChooseEmbedTypeListProps) => {
|
||||
return (
|
||||
<HStack mx="auto">
|
||||
<Stack
|
||||
as={Button}
|
||||
fontWeight="normal"
|
||||
alignItems="center"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
style={{ width: '225px', height: '270px' }}
|
||||
onClick={() => onSelectEmbedType('standard')}
|
||||
whiteSpace={'normal'}
|
||||
spacing="6"
|
||||
isDisabled={disabledTypes.includes('standard')}
|
||||
>
|
||||
<StandardEmbedSvg />
|
||||
<Stack>
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Standard
|
||||
</Text>
|
||||
<Text textColor="gray.500">Embed in a container on your site</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
as={Button}
|
||||
fontWeight="normal"
|
||||
alignItems="center"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
style={{ width: '225px', height: '270px' }}
|
||||
onClick={() => onSelectEmbedType('popup')}
|
||||
whiteSpace={'normal'}
|
||||
spacing="6"
|
||||
isDisabled={disabledTypes.includes('popup')}
|
||||
>
|
||||
<PopupEmbedSvg />
|
||||
<Stack>
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Popup
|
||||
</Text>
|
||||
<Text textColor="gray.500">
|
||||
Embed in a popup window on top of your website
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
as={Button}
|
||||
fontWeight="normal"
|
||||
alignItems="center"
|
||||
variant="outline"
|
||||
colorScheme="gray"
|
||||
style={{ width: '225px', height: '270px' }}
|
||||
onClick={() => onSelectEmbedType('bubble')}
|
||||
whiteSpace={'normal'}
|
||||
spacing="6"
|
||||
isDisabled={disabledTypes.includes('bubble')}
|
||||
>
|
||||
<BubbleEmbedSvg />
|
||||
<Stack>
|
||||
<Text fontSize="lg" fontWeight="semibold">
|
||||
Bubble
|
||||
</Text>
|
||||
<Text textColor="gray.500">
|
||||
Embed in a chat bubble on the corner of your site
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const StandardEmbedSvg = () => (
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="100" height="100" rx="5" fill="#0042DA" />
|
||||
<rect x="10" y="28" width="80" height="42" rx="6" fill="#FF8E20" />
|
||||
<circle cx="18" cy="37" r="5" fill="white" />
|
||||
<rect x="24" y="33" width="45" height="8" rx="4" fill="white" />
|
||||
<circle cx="18" cy="61" r="5" fill="white" />
|
||||
<rect x="24" y="57" width="45" height="8" rx="4" fill="white" />
|
||||
<rect x="31" y="45" width="45" height="8" rx="4" fill="white" />
|
||||
<circle cx="82" cy="49" r="5" fill="white" />
|
||||
<rect x="10" y="9" width="80" height="1" rx="0.5" fill="white" />
|
||||
<rect x="10" y="14" width="80" height="1" rx="0.5" fill="white" />
|
||||
<rect x="10" y="19" width="80" height="1" rx="0.5" fill="white" />
|
||||
<rect x="10" y="80" width="80" height="1" rx="0.5" fill="white" />
|
||||
<rect x="10" y="85" width="80" height="1" rx="0.5" fill="white" />
|
||||
<rect x="10" y="90" width="80" height="1" rx="0.5" fill="white" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const PopupEmbedSvg = () => (
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="100" height="100" rx="5" fill="#0042DA" />
|
||||
<rect x="19" y="20" width="63" height="63" rx="6" fill="#FF8E20" />
|
||||
<circle cx="25.7719" cy="33.7719" r="3.77193" fill="white" />
|
||||
<rect x="31" y="30" width="27" height="8" rx="4" fill="white" />
|
||||
<circle
|
||||
r="3.77193"
|
||||
transform="matrix(-1 0 0 1 75.2281 43.7719)"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
width="22"
|
||||
height="8"
|
||||
rx="4"
|
||||
transform="matrix(-1 0 0 1 70 40)"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="31.0527"
|
||||
y="52"
|
||||
width="26.9473"
|
||||
height="7.54386"
|
||||
rx="3.77193"
|
||||
fill="white"
|
||||
/>
|
||||
<circle cx="25.7719" cy="67.7719" r="3.77193" fill="white" />
|
||||
<rect x="31" y="64" width="27" height="8" rx="4" fill="white" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const BubbleEmbedSvg = () => (
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="100" height="100" rx="5" fill="#0042DA" />
|
||||
<circle cx="85.5" cy="85.5" r="7.5" fill="#FF8E20" />
|
||||
</svg>
|
||||
)
|
@ -1,132 +0,0 @@
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { OrderedList, ListItem, Tag } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { BubbleParams } from 'typebot-js'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
import { ChatEmbedCode } from '../../codeSnippets/Chat/EmbedCode'
|
||||
import { ChatEmbedSettings } from '../../codeSnippets/Chat/EmbedSettings'
|
||||
import { StandardEmbedWindowSettings } from '../../codeSnippets/Container/EmbedSettings'
|
||||
import {
|
||||
parseInitContainerCode,
|
||||
typebotJsHtml,
|
||||
} from '../../codeSnippets/params'
|
||||
import { PopupEmbedCode } from '../../codeSnippets/Popup/EmbedCode'
|
||||
import { PopupEmbedSettings } from '../../codeSnippets/Popup/EmbedSettings'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
|
||||
type GtmInstructionsProps = {
|
||||
type: 'standard' | 'popup' | 'bubble'
|
||||
publicId: string
|
||||
}
|
||||
|
||||
export const GtmInstructions = ({ type, publicId }: GtmInstructionsProps) => {
|
||||
switch (type) {
|
||||
case 'standard': {
|
||||
return <StandardInstructions publicId={publicId} />
|
||||
}
|
||||
case 'popup': {
|
||||
return <PopupInstructions />
|
||||
}
|
||||
case 'bubble': {
|
||||
return <BubbleInstructions />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
|
||||
const [windowSizes, setWindowSizes] = useState({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const jsCode = parseInitContainerCode({
|
||||
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${publicId}`,
|
||||
})
|
||||
const headCode = `${typebotJsHtml}
|
||||
<script>
|
||||
${jsCode}
|
||||
</script>`
|
||||
|
||||
const elementCode = `<div id="typebot-container" style="height: ${windowSizes.height}; width: ${windowSizes.width}"></div>`
|
||||
return (
|
||||
<OrderedList spacing={2} mb={4}>
|
||||
<ListItem>
|
||||
On your GTM account dashboard, click on <Tag>Add a new tag</Tag>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Choose Custom <Tag>HTML tag</Tag> type
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Paste the code below:
|
||||
<CodeEditor value={headCode} mt={2} isReadOnly lang="html" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
On your webpage, you need to have an element on which the typebot will
|
||||
go. It needs to have the id <Tag>typebot-container</Tag>:
|
||||
<StandardEmbedWindowSettings
|
||||
my={4}
|
||||
onUpdateWindowSettings={(sizes) =>
|
||||
setWindowSizes({
|
||||
height: sizes.heightLabel,
|
||||
width: sizes.widthLabel,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<CodeEditor value={elementCode} mt={2} isReadOnly lang="html" />
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
||||
|
||||
const PopupInstructions = () => {
|
||||
const [inputValue, setInputValue] = useState<number>()
|
||||
|
||||
return (
|
||||
<OrderedList spacing={2} mb={4}>
|
||||
<ListItem>
|
||||
On your GTM account dashboard, click on <Tag>Add a new tag</Tag>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Choose Custom <Tag>HTML tag</Tag> type
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Paste the code below:
|
||||
<PopupEmbedSettings
|
||||
my={4}
|
||||
onUpdateSettings={(settings) => setInputValue(settings.delay)}
|
||||
/>
|
||||
<PopupEmbedCode delay={inputValue} />
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
||||
|
||||
const BubbleInstructions = () => {
|
||||
const [inputValues, setInputValues] = useState<
|
||||
Pick<BubbleParams, 'proactiveMessage' | 'button'>
|
||||
>({
|
||||
proactiveMessage: undefined,
|
||||
button: {
|
||||
color: '',
|
||||
iconUrl: '',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<OrderedList spacing={2} mb={4}>
|
||||
<ListItem>
|
||||
On your GTM account dashboard, click on <Tag>Add a new tag</Tag>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Choose Custom <Tag>HTML tag</Tag> type
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Paste the code below:
|
||||
<ChatEmbedSettings
|
||||
onUpdateSettings={(settings) => setInputValues({ ...settings })}
|
||||
/>
|
||||
<ChatEmbedCode my={4} {...inputValues} />
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import React, { useState } from 'react'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
import { EmbedModal } from '../../EmbedModal'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { GtmInstructions } from './instructions/GtmInstructions'
|
||||
|
||||
export const GtmModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
isPublished,
|
||||
publicId,
|
||||
}: ModalProps) => {
|
||||
const [selectedEmbedType, setSelectedEmbedType] = useState<
|
||||
'standard' | 'popup' | 'bubble' | undefined
|
||||
>()
|
||||
return (
|
||||
<EmbedModal
|
||||
titlePrefix="GTM"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
isPublished={isPublished}
|
||||
onSelectEmbedType={setSelectedEmbedType}
|
||||
selectedEmbedType={selectedEmbedType}
|
||||
>
|
||||
{isDefined(selectedEmbedType) && (
|
||||
<GtmInstructions type={selectedEmbedType} publicId={publicId} />
|
||||
)}
|
||||
</EmbedModal>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './GtmModal'
|
@ -1,70 +0,0 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
IconButton,
|
||||
Heading,
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import React, { useState } from 'react'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
import { ChooseEmbedTypeList } from '../ChooseEmbedTypeList'
|
||||
import { capitalize } from 'utils'
|
||||
import { GtmInstructions } from './GtmInstructions'
|
||||
import { AlertInfo } from '@/components/AlertInfo'
|
||||
|
||||
export const GtmModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
isPublished,
|
||||
publicId,
|
||||
}: ModalProps) => {
|
||||
const [chosenEmbedType, setChosenEmbedType] = useState<
|
||||
'standard' | 'popup' | 'bubble' | undefined
|
||||
>()
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size={!chosenEmbedType ? '2xl' : 'xl'}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
{chosenEmbedType && (
|
||||
<IconButton
|
||||
icon={<ChevronLeftIcon />}
|
||||
aria-label="back"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
mr={2}
|
||||
onClick={() => setChosenEmbedType(undefined)}
|
||||
/>
|
||||
)}
|
||||
<Heading size="md">
|
||||
Javascript {chosenEmbedType && `- ${capitalize(chosenEmbedType)}`}
|
||||
</Heading>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{!isPublished && (
|
||||
<AlertInfo mb="2">You need to publish your bot first.</AlertInfo>
|
||||
)}
|
||||
{!chosenEmbedType ? (
|
||||
<ChooseEmbedTypeList onSelectEmbedType={setChosenEmbedType} />
|
||||
) : (
|
||||
<GtmInstructions type={chosenEmbedType} publicId={publicId} />
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { OrderedList, ListItem, Stack, Text, Code } from '@chakra-ui/react'
|
||||
import { BubbleProps } from '@typebot.io/js'
|
||||
import { useState } from 'react'
|
||||
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
|
||||
import { parseDefaultBubbleTheme } from '../../Javascript/instructions/JavascriptBubbleInstructions'
|
||||
import { JavascriptBubbleSnippet } from '../../Javascript/JavascriptBubbleSnippet'
|
||||
|
||||
export const GtmBubbleInstructions = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const [theme, setTheme] = useState<BubbleProps['theme']>(
|
||||
parseDefaultBubbleTheme(typebot)
|
||||
)
|
||||
const [previewMessage, setPreviewMessage] =
|
||||
useState<BubbleProps['previewMessage']>()
|
||||
|
||||
return (
|
||||
<OrderedList spacing={4} pl={5}>
|
||||
<ListItem>
|
||||
On your GTM account dashboard, click on <Code>Add a new tag</Code>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Choose Custom <Code>HTML tag</Code> type
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<BubbleSettings
|
||||
theme={theme}
|
||||
previewMessage={previewMessage}
|
||||
defaultPreviewMessageAvatar={
|
||||
typebot?.theme.chat.hostAvatar?.url ?? ''
|
||||
}
|
||||
onThemeChange={setTheme}
|
||||
onPreviewMessageChange={setPreviewMessage}
|
||||
/>
|
||||
<Text>Paste the code below:</Text>
|
||||
<JavascriptBubbleSnippet
|
||||
theme={theme}
|
||||
previewMessage={previewMessage}
|
||||
/>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { GtmBubbleInstructions } from './GtmBubbleInstructions'
|
||||
import { GtmPopupInstructions } from './GtmPopupInstructions'
|
||||
import { GtmStandardInstructions } from './GtmStandardInstructions'
|
||||
|
||||
type GtmInstructionsProps = {
|
||||
type: 'standard' | 'popup' | 'bubble'
|
||||
publicId: string
|
||||
}
|
||||
|
||||
export const GtmInstructions = ({ type, publicId }: GtmInstructionsProps) => {
|
||||
switch (type) {
|
||||
case 'standard': {
|
||||
return <GtmStandardInstructions publicId={publicId} />
|
||||
}
|
||||
case 'popup': {
|
||||
return <GtmPopupInstructions />
|
||||
}
|
||||
case 'bubble': {
|
||||
return <GtmBubbleInstructions />
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { OrderedList, ListItem, Code, Stack, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { PopupSettings } from '../../../settings/PopupSettings'
|
||||
import { JavascriptPopupSnippet } from '../../Javascript/JavascriptPopupSnippet'
|
||||
|
||||
export const GtmPopupInstructions = () => {
|
||||
const [inputValue, setInputValue] = useState<number>()
|
||||
|
||||
return (
|
||||
<OrderedList spacing={4} pl={5}>
|
||||
<ListItem>
|
||||
On your GTM account dashboard, click on <Code>Add a new tag</Code>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Choose Custom <Code>HTML tag</Code> type
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<PopupSettings
|
||||
onUpdateSettings={(settings) =>
|
||||
setInputValue(settings.autoShowDelay)
|
||||
}
|
||||
/>
|
||||
<Text>Paste the code below:</Text>
|
||||
<JavascriptPopupSnippet autoShowDelay={inputValue} />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { OrderedList, ListItem, Code, Stack, Text } from '@chakra-ui/react'
|
||||
import { Typebot } from 'models'
|
||||
import { useState } from 'react'
|
||||
import { StandardSettings } from '../../../settings/StandardSettings'
|
||||
import {
|
||||
parseStandardElementCode,
|
||||
parseStandardHeadCode,
|
||||
} from '../../Javascript/JavascriptStandardSnippet'
|
||||
|
||||
export const GtmStandardInstructions = ({
|
||||
publicId,
|
||||
}: Pick<Typebot, 'publicId'>) => {
|
||||
const [windowSizes, setWindowSizes] = useState<{
|
||||
height: string
|
||||
width?: string
|
||||
}>({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const headCode = parseStandardHeadCode(publicId)
|
||||
|
||||
const elementCode = parseStandardElementCode(
|
||||
windowSizes.width,
|
||||
windowSizes.height
|
||||
)
|
||||
|
||||
return (
|
||||
<OrderedList spacing={4} pl={5}>
|
||||
<ListItem>
|
||||
On your GTM account dashboard, click on <Code>Add a new tag</Code>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Choose Custom <Code>HTML tag</Code> type
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<Text>Paste the code below:</Text>
|
||||
<CodeEditor value={headCode} isReadOnly lang="html" />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<StandardSettings
|
||||
onUpdateWindowSettings={(sizes) =>
|
||||
setWindowSizes({
|
||||
height: sizes.heightLabel,
|
||||
width: sizes.widthLabel,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Text>
|
||||
On your web page, you need to have an element on which the typebot
|
||||
will go:
|
||||
</Text>
|
||||
<CodeEditor value={elementCode} isReadOnly lang="html" />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
@ -11,12 +11,15 @@ import {
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { StandardEmbedWindowSettings } from '../codeSnippets/Container/EmbedSettings'
|
||||
import { IframeEmbedCode } from '../codeSnippets/Iframe/EmbedCode'
|
||||
import { ModalProps } from '../EmbedButton'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
import { StandardSettings } from '../../settings/StandardSettings'
|
||||
import { IframeSnippet } from './IframeSnippet'
|
||||
|
||||
export const IframeModal = ({ isPublished, isOpen, onClose }: ModalProps) => {
|
||||
const [inputValues, setInputValues] = useState({
|
||||
const [inputValues, setInputValues] = useState<{
|
||||
heightLabel: string
|
||||
widthLabel?: string
|
||||
}>({
|
||||
heightLabel: '100%',
|
||||
widthLabel: '100%',
|
||||
})
|
||||
@ -27,17 +30,21 @@ export const IframeModal = ({ isPublished, isOpen, onClose }: ModalProps) => {
|
||||
<ModalContent>
|
||||
<ModalHeader>Iframe</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing={4}>
|
||||
<ModalBody as={Stack} spacing={4} pt="0">
|
||||
{!isPublished && (
|
||||
<AlertInfo>You need to publish your bot first.</AlertInfo>
|
||||
)}
|
||||
<Text>Paste this anywhere in your HTML code:</Text>
|
||||
<StandardEmbedWindowSettings
|
||||
<StandardSettings
|
||||
onUpdateWindowSettings={(settings) =>
|
||||
setInputValues({ ...settings })
|
||||
}
|
||||
/>
|
||||
<IframeEmbedCode {...inputValues} />
|
||||
<Text>Paste this anywhere in your HTML code:</Text>
|
||||
|
||||
<IframeSnippet
|
||||
widthLabel={inputValues.widthLabel ?? '100%'}
|
||||
heightLabel={inputValues.heightLabel}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
@ -2,21 +2,24 @@ import { FlexProps } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import prettier from 'prettier/standalone'
|
||||
import parserHtml from 'prettier/parser-html'
|
||||
|
||||
type Props = {
|
||||
widthLabel: string
|
||||
heightLabel: string
|
||||
onCopied?: () => void
|
||||
}
|
||||
export const IframeEmbedCode = ({
|
||||
widthLabel,
|
||||
heightLabel,
|
||||
}: Props & FlexProps) => {
|
||||
} & FlexProps
|
||||
|
||||
export const IframeSnippet = ({ widthLabel, heightLabel }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const src = `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${
|
||||
typebot?.publicId
|
||||
}`
|
||||
const code = `<iframe src="${src}" width="${widthLabel}" height="${heightLabel}" style="border: none"></iframe>`
|
||||
const code = prettier.format(
|
||||
`<iframe src="${src}" style="border: none; width='${widthLabel}'; height='${heightLabel}'"></iframe>`,
|
||||
{ parser: 'html', plugins: [parserHtml] }
|
||||
)
|
||||
|
||||
return <CodeEditor value={code} lang="html" isReadOnly />
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import prettier from 'prettier/standalone'
|
||||
import parserHtml from 'prettier/parser-html'
|
||||
import { parseInitBubbleCode } from '../../snippetParsers'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { BubbleProps } from '@typebot.io/js'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
|
||||
type Props = Pick<BubbleProps, 'theme' | 'previewMessage'>
|
||||
|
||||
export const JavascriptBubbleSnippet = ({ theme, previewMessage }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
const snippet = prettier.format(
|
||||
`<script type="module">${parseInitBubbleCode({
|
||||
typebot: typebot?.publicId ?? '',
|
||||
apiHost: isCloudProdInstance
|
||||
? undefined
|
||||
: env('VIEWER_INTERNAL_URL') ?? getViewerUrl(),
|
||||
theme: {
|
||||
...theme,
|
||||
chatWindow: {
|
||||
backgroundColor: typebot?.theme.general.background.content ?? '#fff',
|
||||
},
|
||||
},
|
||||
previewMessage,
|
||||
})}</script>`,
|
||||
{
|
||||
parser: 'html',
|
||||
plugins: [parserHtml],
|
||||
}
|
||||
)
|
||||
|
||||
return <CodeEditor value={snippet} lang="html" isReadOnly />
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import { Stack, Tag, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { BubbleParams } from 'typebot-js'
|
||||
import { ChatEmbedCode } from '../../codeSnippets/Chat/EmbedCode'
|
||||
import { ChatEmbedSettings } from '../../codeSnippets/Chat/EmbedSettings'
|
||||
import { ContainerEmbedCode } from '../../codeSnippets/Container/EmbedCode'
|
||||
import { StandardEmbedWindowSettings } from '../../codeSnippets/Container/EmbedSettings'
|
||||
import { PopupEmbedCode } from '../../codeSnippets/Popup/EmbedCode'
|
||||
import { PopupEmbedSettings } from '../../codeSnippets/Popup/EmbedSettings'
|
||||
|
||||
type JavascriptInstructionsProps = {
|
||||
type: 'standard' | 'popup' | 'bubble'
|
||||
}
|
||||
|
||||
export const JavascriptInstructions = ({
|
||||
type,
|
||||
}: JavascriptInstructionsProps) => {
|
||||
switch (type) {
|
||||
case 'standard': {
|
||||
return <StandardInstructions />
|
||||
}
|
||||
case 'popup': {
|
||||
return <PopupInstructions />
|
||||
}
|
||||
case 'bubble': {
|
||||
return <BubbleInstructions />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const StandardInstructions = () => {
|
||||
const [inputValues, setInputValues] = useState({
|
||||
heightLabel: '100%',
|
||||
widthLabel: '100%',
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Text>
|
||||
Paste this anywhere in the <Tag>body</Tag>
|
||||
</Text>
|
||||
<StandardEmbedWindowSettings
|
||||
onUpdateWindowSettings={(settings) => setInputValues({ ...settings })}
|
||||
/>
|
||||
<ContainerEmbedCode withStarterVariables={true} {...inputValues} mt={4} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const PopupInstructions = () => {
|
||||
const [inputValue, setInputValue] = useState<number>()
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Text>
|
||||
Paste this anywhere in the <Tag>body</Tag>
|
||||
</Text>
|
||||
<PopupEmbedSettings
|
||||
mb={4}
|
||||
onUpdateSettings={(settings) => setInputValue(settings.delay)}
|
||||
/>
|
||||
<PopupEmbedCode delay={inputValue} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const BubbleInstructions = () => {
|
||||
const [inputValues, setInputValues] = useState<
|
||||
Pick<BubbleParams, 'proactiveMessage' | 'button'>
|
||||
>({
|
||||
proactiveMessage: undefined,
|
||||
button: {
|
||||
color: '',
|
||||
iconUrl: '',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Text>
|
||||
Paste this anywhere in the <Tag>body</Tag>
|
||||
</Text>
|
||||
<ChatEmbedSettings
|
||||
onUpdateSettings={(settings) => setInputValues({ ...settings })}
|
||||
/>
|
||||
<ChatEmbedCode {...inputValues} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,69 +1,29 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
IconButton,
|
||||
Heading,
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import React, { useState } from 'react'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
import { ChooseEmbedTypeList } from '../ChooseEmbedTypeList'
|
||||
import { capitalize } from 'utils'
|
||||
import { JavascriptInstructions } from './JavascriptInstructions'
|
||||
import { AlertInfo } from '@/components/AlertInfo'
|
||||
import { EmbedModal } from '../../EmbedModal'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { JavascriptInstructions } from './instructions/JavascriptInstructions'
|
||||
|
||||
export const JavascriptModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
isPublished,
|
||||
}: ModalProps) => {
|
||||
const [chosenEmbedType, setChosenEmbedType] = useState<
|
||||
const [selectedEmbedType, setSelectedEmbedType] = useState<
|
||||
'standard' | 'popup' | 'bubble' | undefined
|
||||
>()
|
||||
return (
|
||||
<Modal
|
||||
<EmbedModal
|
||||
titlePrefix="Javascript"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size={!chosenEmbedType ? '2xl' : 'xl'}
|
||||
isPublished={isPublished}
|
||||
onSelectEmbedType={setSelectedEmbedType}
|
||||
selectedEmbedType={selectedEmbedType}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
{chosenEmbedType && (
|
||||
<IconButton
|
||||
icon={<ChevronLeftIcon />}
|
||||
aria-label="back"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
mr={2}
|
||||
onClick={() => setChosenEmbedType(undefined)}
|
||||
/>
|
||||
)}
|
||||
<Heading size="md">
|
||||
Javascript {chosenEmbedType && `- ${capitalize(chosenEmbedType)}`}
|
||||
</Heading>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{!isPublished && (
|
||||
<AlertInfo mb="2">You need to publish your bot first.</AlertInfo>
|
||||
)}
|
||||
{!chosenEmbedType ? (
|
||||
<ChooseEmbedTypeList onSelectEmbedType={setChosenEmbedType} />
|
||||
) : (
|
||||
<JavascriptInstructions type={chosenEmbedType} />
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{isDefined(selectedEmbedType) && (
|
||||
<JavascriptInstructions type={selectedEmbedType} />
|
||||
)}
|
||||
</EmbedModal>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import parserHtml from 'prettier/parser-html'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { parseInitPopupCode } from '../../snippetParsers'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { PopupProps } from '@typebot.io/js'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
|
||||
type Props = Pick<PopupProps, 'autoShowDelay'>
|
||||
|
||||
export const JavascriptPopupSnippet = ({ autoShowDelay }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const snippet = prettier.format(
|
||||
createSnippet({
|
||||
typebot: typebot?.publicId ?? '',
|
||||
apiHost: isCloudProdInstance
|
||||
? undefined
|
||||
: env('VIEWER_INTERNAL_URL') ?? getViewerUrl(),
|
||||
autoShowDelay,
|
||||
}),
|
||||
{
|
||||
parser: 'html',
|
||||
plugins: [parserHtml],
|
||||
}
|
||||
)
|
||||
return <CodeEditor value={snippet} lang="html" isReadOnly />
|
||||
}
|
||||
|
||||
const createSnippet = (params: PopupProps): string => {
|
||||
const jsCode = parseInitPopupCode(params)
|
||||
return `<script type="module">${jsCode}</script>`
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import parserHtml from 'prettier/parser-html'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { parseInitStandardCode } from '../../snippetParsers'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
|
||||
type Props = {
|
||||
widthLabel?: string
|
||||
heightLabel?: string
|
||||
}
|
||||
|
||||
export const JavascriptStandardSnippet = ({
|
||||
widthLabel,
|
||||
heightLabel,
|
||||
}: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
const snippet = prettier.format(
|
||||
`${parseStandardHeadCode(typebot?.publicId)}
|
||||
${parseStandardElementCode(widthLabel, heightLabel)}`,
|
||||
{
|
||||
parser: 'html',
|
||||
plugins: [parserHtml],
|
||||
}
|
||||
)
|
||||
|
||||
return <CodeEditor value={snippet} lang="html" isReadOnly />
|
||||
}
|
||||
|
||||
export const parseStandardHeadCode = (publicId?: string | null) =>
|
||||
prettier.format(
|
||||
`<script type="module">${parseInitStandardCode({
|
||||
typebot: publicId ?? '',
|
||||
apiHost: isCloudProdInstance
|
||||
? undefined
|
||||
: env('VIEWER_INTERNAL_URL') ?? getViewerUrl(),
|
||||
})}</script>`,
|
||||
{ parser: 'html', plugins: [parserHtml] }
|
||||
)
|
||||
|
||||
export const parseStandardElementCode = (width?: string, height?: string) => {
|
||||
if (!width && !height) return '<typebot-standard></typebot-standard>'
|
||||
return prettier.format(
|
||||
`<typebot-standard style="${width ? `width: ${width}; ` : ''}${
|
||||
height ? `height: ${height}; ` : ''
|
||||
}"></typebot-standard>`,
|
||||
{ parser: 'html', plugins: [parserHtml] }
|
||||
)
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { Stack, Code, Text } from '@chakra-ui/react'
|
||||
import { BubbleProps } from '@typebot.io/js'
|
||||
import { Typebot } from 'models'
|
||||
import { useState } from 'react'
|
||||
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
|
||||
import { JavascriptBubbleSnippet } from '../JavascriptBubbleSnippet'
|
||||
|
||||
export const parseDefaultBubbleTheme = (typebot?: Typebot) => ({
|
||||
button: {
|
||||
backgroundColor: typebot?.theme.chat.buttons.backgroundColor,
|
||||
iconColor: typebot?.theme.chat.buttons.color,
|
||||
},
|
||||
previewMessage: {
|
||||
backgroundColor: typebot?.theme.general.background.content ?? 'white',
|
||||
textColor: 'black',
|
||||
},
|
||||
})
|
||||
|
||||
export const JavascriptBubbleInstructions = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const [theme, setTheme] = useState<BubbleProps['theme']>(
|
||||
parseDefaultBubbleTheme(typebot)
|
||||
)
|
||||
const [previewMessage, setPreviewMessage] =
|
||||
useState<BubbleProps['previewMessage']>()
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<BubbleSettings
|
||||
theme={theme}
|
||||
previewMessage={previewMessage}
|
||||
defaultPreviewMessageAvatar={typebot?.theme.chat.hostAvatar?.url ?? ''}
|
||||
onThemeChange={setTheme}
|
||||
onPreviewMessageChange={setPreviewMessage}
|
||||
/>
|
||||
<Text>
|
||||
Paste this anywhere in the <Code>{'<body>'}</Code>:
|
||||
</Text>
|
||||
<JavascriptBubbleSnippet theme={theme} previewMessage={previewMessage} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { JavascriptBubbleInstructions } from './JavascriptBubbleInstructions'
|
||||
import { JavascriptPopupInstructions } from './JavascriptPopupInstructions'
|
||||
import { JavascriptStandardInstructions } from './JavascriptStandardInstructions'
|
||||
|
||||
type JavascriptInstructionsProps = {
|
||||
type: 'standard' | 'popup' | 'bubble'
|
||||
}
|
||||
|
||||
export const JavascriptInstructions = ({
|
||||
type,
|
||||
}: JavascriptInstructionsProps) => {
|
||||
switch (type) {
|
||||
case 'standard': {
|
||||
return <JavascriptStandardInstructions />
|
||||
}
|
||||
case 'popup': {
|
||||
return <JavascriptPopupInstructions />
|
||||
}
|
||||
case 'bubble': {
|
||||
return <JavascriptBubbleInstructions />
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { Stack, Code, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { PopupSettings } from '../../../settings/PopupSettings'
|
||||
import { JavascriptPopupSnippet } from '../JavascriptPopupSnippet'
|
||||
|
||||
export const JavascriptPopupInstructions = () => {
|
||||
const [inputValue, setInputValue] = useState<number>()
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<PopupSettings
|
||||
onUpdateSettings={(settings) => setInputValue(settings.autoShowDelay)}
|
||||
/>
|
||||
<Text>
|
||||
Paste this anywhere in the <Code>{'<body>'}</Code>:
|
||||
</Text>
|
||||
<JavascriptPopupSnippet autoShowDelay={inputValue} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { Stack, Code, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { StandardSettings } from '../../../settings/StandardSettings'
|
||||
import { JavascriptStandardSnippet } from '../JavascriptStandardSnippet'
|
||||
|
||||
export const JavascriptStandardInstructions = () => {
|
||||
const [inputValues, setInputValues] = useState<{
|
||||
heightLabel: string
|
||||
widthLabel?: string
|
||||
}>({
|
||||
heightLabel: '100%',
|
||||
widthLabel: '100%',
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<StandardSettings
|
||||
onUpdateWindowSettings={(settings) => setInputValues({ ...settings })}
|
||||
/>
|
||||
<Text>
|
||||
Paste this anywhere in the <Code>{'<body>'}</Code>:
|
||||
</Text>
|
||||
<JavascriptStandardSnippet {...inputValues} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -10,11 +10,13 @@ import {
|
||||
ModalBody,
|
||||
OrderedList,
|
||||
ListItem,
|
||||
Tag,
|
||||
Code,
|
||||
InputGroup,
|
||||
Input,
|
||||
InputRightElement,
|
||||
ModalFooter,
|
||||
Text,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
import { ModalProps } from '../EmbedButton'
|
||||
@ -37,28 +39,30 @@ export const NotionModal = ({
|
||||
{!isPublished && (
|
||||
<AlertInfo mb="4">You need to publish your bot first.</AlertInfo>
|
||||
)}
|
||||
<OrderedList spacing={3}>
|
||||
<OrderedList spacing={4}>
|
||||
<ListItem>
|
||||
Type <Tag>/embed</Tag>
|
||||
Type <Code>/embed</Code>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Paste your typebot URL
|
||||
<InputGroup size="md" mt={2}>
|
||||
<Input
|
||||
pr="4.5rem"
|
||||
type={'text'}
|
||||
defaultValue={`${
|
||||
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
|
||||
}/${publicId}`}
|
||||
/>
|
||||
<InputRightElement width="4.5rem">
|
||||
<CopyButton
|
||||
textToCopy={`${
|
||||
<Stack>
|
||||
<Text>Paste your typebot URL</Text>
|
||||
<InputGroup size="sm">
|
||||
<Input
|
||||
type={'text'}
|
||||
defaultValue={`${
|
||||
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
|
||||
}/${publicId}`}
|
||||
/>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
<InputRightElement width="60px">
|
||||
<CopyButton
|
||||
size="sm"
|
||||
textToCopy={`${
|
||||
env('VIEWER_INTERNAL_URL') ?? getViewerUrl()
|
||||
}/${publicId}`}
|
||||
/>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
</ModalBody>
|
||||
|
@ -0,0 +1,25 @@
|
||||
import React, { useState } from 'react'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { EmbedModal } from '../EmbedModal'
|
||||
import { JavascriptInstructions } from './Javascript/instructions/JavascriptInstructions'
|
||||
import { ModalProps } from '../EmbedButton'
|
||||
|
||||
export const OtherModal = ({ isOpen, onClose, isPublished }: ModalProps) => {
|
||||
const [selectedEmbedType, setSelectedEmbedType] = useState<
|
||||
'standard' | 'popup' | 'bubble' | undefined
|
||||
>()
|
||||
return (
|
||||
<EmbedModal
|
||||
titlePrefix="Other"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
isPublished={isPublished}
|
||||
onSelectEmbedType={setSelectedEmbedType}
|
||||
selectedEmbedType={selectedEmbedType}
|
||||
>
|
||||
{isDefined(selectedEmbedType) && (
|
||||
<JavascriptInstructions type={selectedEmbedType} />
|
||||
)}
|
||||
</EmbedModal>
|
||||
)
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
|
||||
export const InstallReactPackageSnippet = () => {
|
||||
return (
|
||||
<CodeEditor
|
||||
value={`npm install @typebot.io/js @typebot.io/react`}
|
||||
isReadOnly
|
||||
lang="shell"
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { BubbleProps } from '@typebot.io/js'
|
||||
import parserBabel from 'prettier/parser-babel'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { parseReactBubbleProps } from '../../snippetParsers'
|
||||
|
||||
export const ReactBubbleSnippet = ({
|
||||
theme,
|
||||
previewMessage,
|
||||
}: Pick<BubbleProps, 'theme' | 'previewMessage'>) => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
const snippet = prettier.format(
|
||||
`import { Bubble } from "@typebot.io/react";
|
||||
|
||||
const App = () => {
|
||||
return <Bubble ${parseReactBubbleProps({
|
||||
typebot: typebot?.publicId ?? '',
|
||||
theme,
|
||||
previewMessage,
|
||||
})}/>
|
||||
}`,
|
||||
{
|
||||
parser: 'babel',
|
||||
plugins: [parserBabel],
|
||||
}
|
||||
)
|
||||
|
||||
return <CodeEditor value={snippet} lang="javascript" isReadOnly />
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { BubbleParams } from 'typebot-js'
|
||||
import { ChatEmbedSettings } from '../../codeSnippets/Chat/EmbedSettings'
|
||||
import { StandardEmbedWindowSettings } from '../../codeSnippets/Container/EmbedSettings'
|
||||
import { PopupEmbedSettings } from '../../codeSnippets/Popup/EmbedSettings'
|
||||
import {
|
||||
StandardReactDiv,
|
||||
PopupReactCode,
|
||||
ChatReactCode,
|
||||
} from '../../codeSnippets/ReactCode'
|
||||
|
||||
type Props = {
|
||||
type: 'standard' | 'popup' | 'bubble'
|
||||
}
|
||||
|
||||
export const ReactInstructions = ({ type }: Props) => {
|
||||
switch (type) {
|
||||
case 'standard': {
|
||||
return <StandardInstructions />
|
||||
}
|
||||
case 'popup': {
|
||||
return <PopupInstructions />
|
||||
}
|
||||
case 'bubble': {
|
||||
return <BubbleInstructions />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const StandardInstructions = () => {
|
||||
const [inputValues, setInputValues] = useState({
|
||||
heightLabel: '100%',
|
||||
widthLabel: '100%',
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<InstallPackageInstruction />
|
||||
<StandardEmbedWindowSettings
|
||||
onUpdateWindowSettings={(settings) => setInputValues({ ...settings })}
|
||||
/>
|
||||
<Text>Insert the typebot container</Text>
|
||||
<StandardReactDiv {...inputValues} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const PopupInstructions = () => {
|
||||
const [inputValue, setInputValue] = useState<number>()
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<InstallPackageInstruction />
|
||||
<PopupEmbedSettings
|
||||
onUpdateSettings={(settings) => setInputValue(settings.delay)}
|
||||
/>
|
||||
<Text>Initialize the typebot</Text>
|
||||
<PopupReactCode withStarterVariables={true} delay={inputValue} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const BubbleInstructions = () => {
|
||||
const [inputValues, setInputValues] = useState<
|
||||
Pick<BubbleParams, 'proactiveMessage' | 'button'>
|
||||
>({
|
||||
proactiveMessage: undefined,
|
||||
button: {
|
||||
color: '',
|
||||
iconUrl: '',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<InstallPackageInstruction />
|
||||
<ChatEmbedSettings
|
||||
onUpdateSettings={(settings) => setInputValues({ ...settings })}
|
||||
/>
|
||||
<Text>Initialize the typebot</Text>
|
||||
<ChatReactCode withStarterVariables={true} {...inputValues} mt={4} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const InstallPackageInstruction = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Text>Install the package:</Text>
|
||||
<CodeEditor value={`npm install typebot-js`} isReadOnly />
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1,65 +1,25 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
IconButton,
|
||||
Heading,
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import React, { useState } from 'react'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
import { ChooseEmbedTypeList } from '../ChooseEmbedTypeList'
|
||||
import { capitalize } from 'utils'
|
||||
import { ReactInstructions } from './ReactInstructions'
|
||||
import { AlertInfo } from '@/components/AlertInfo'
|
||||
import { EmbedModal } from '../../EmbedModal'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { ReactInstructions } from './instructions/ReactInstructions'
|
||||
|
||||
export const ReactModal = ({ isOpen, onClose, isPublished }: ModalProps) => {
|
||||
const [chosenEmbedType, setChosenEmbedType] = useState<
|
||||
const [selectedEmbedType, setSelectedEmbedType] = useState<
|
||||
'standard' | 'popup' | 'bubble' | undefined
|
||||
>()
|
||||
return (
|
||||
<Modal
|
||||
<EmbedModal
|
||||
titlePrefix="React"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size={!chosenEmbedType ? '2xl' : 'xl'}
|
||||
isPublished={isPublished}
|
||||
onSelectEmbedType={setSelectedEmbedType}
|
||||
selectedEmbedType={selectedEmbedType}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
{chosenEmbedType && (
|
||||
<IconButton
|
||||
icon={<ChevronLeftIcon />}
|
||||
aria-label="back"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
mr={2}
|
||||
onClick={() => setChosenEmbedType(undefined)}
|
||||
/>
|
||||
)}
|
||||
<Heading size="md">
|
||||
React {chosenEmbedType && `- ${capitalize(chosenEmbedType)}`}
|
||||
</Heading>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{!isPublished && (
|
||||
<AlertInfo mb="2">You need to publish your bot first.</AlertInfo>
|
||||
)}
|
||||
{!chosenEmbedType ? (
|
||||
<ChooseEmbedTypeList onSelectEmbedType={setChosenEmbedType} />
|
||||
) : (
|
||||
<ReactInstructions type={chosenEmbedType} />
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{isDefined(selectedEmbedType) && (
|
||||
<ReactInstructions type={selectedEmbedType} />
|
||||
)}
|
||||
</EmbedModal>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { PopupProps } from '@typebot.io/js'
|
||||
import parserBabel from 'prettier/parser-babel'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { parseReactPopupProps } from '../../snippetParsers'
|
||||
|
||||
export const ReactPopupSnippet = ({
|
||||
autoShowDelay,
|
||||
}: Pick<PopupProps, 'autoShowDelay'>) => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
const snippet = prettier.format(
|
||||
`import { Popup } from "@typebot.io/react";
|
||||
|
||||
const App = () => {
|
||||
return <Popup ${parseReactPopupProps({
|
||||
typebot: typebot?.publicId ?? '',
|
||||
autoShowDelay,
|
||||
})}/>;
|
||||
}`,
|
||||
{
|
||||
parser: 'babel',
|
||||
plugins: [parserBabel],
|
||||
}
|
||||
)
|
||||
|
||||
return <CodeEditor value={snippet} lang="javascript" isReadOnly />
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import parserBabel from 'prettier/parser-babel'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { parseReactBotProps } from '../../snippetParsers'
|
||||
|
||||
type ReactStandardSnippetProps = { widthLabel?: string; heightLabel: string }
|
||||
|
||||
export const ReactStandardSnippet = ({
|
||||
widthLabel,
|
||||
heightLabel,
|
||||
}: ReactStandardSnippetProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const snippet = prettier.format(
|
||||
`import { Standard } from "@typebot.io/react";
|
||||
|
||||
const App = () => {
|
||||
return <Standard ${parseReactBotProps({
|
||||
typebot: typebot?.publicId ?? '',
|
||||
})} style={{width: "${widthLabel}", height: "${heightLabel}"}} />
|
||||
}`,
|
||||
{
|
||||
parser: 'babel',
|
||||
plugins: [parserBabel],
|
||||
}
|
||||
)
|
||||
return <CodeEditor value={snippet} lang="javascript" isReadOnly />
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { ListItem, OrderedList, Stack, Text } from '@chakra-ui/react'
|
||||
import { BubbleProps } from '@typebot.io/js'
|
||||
import { useState } from 'react'
|
||||
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
|
||||
import { InstallReactPackageSnippet } from '../InstallReactPackageSnippet'
|
||||
import { ReactBubbleSnippet } from '../ReactBubbleSnippet'
|
||||
import { parseDefaultBubbleTheme } from '../../Javascript/instructions/JavascriptBubbleInstructions'
|
||||
|
||||
export const ReactBubbleInstructions = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const [theme, setTheme] = useState<BubbleProps['theme']>(
|
||||
parseDefaultBubbleTheme(typebot)
|
||||
)
|
||||
const [previewMessage, setPreviewMessage] =
|
||||
useState<BubbleProps['previewMessage']>()
|
||||
|
||||
return (
|
||||
<OrderedList spacing={4} pl={5}>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<Text>Install the packages</Text>
|
||||
<InstallReactPackageSnippet />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<BubbleSettings
|
||||
theme={theme}
|
||||
previewMessage={previewMessage}
|
||||
defaultPreviewMessageAvatar={
|
||||
typebot?.theme.chat.hostAvatar?.url ?? ''
|
||||
}
|
||||
onThemeChange={setTheme}
|
||||
onPreviewMessageChange={setPreviewMessage}
|
||||
/>
|
||||
<ReactBubbleSnippet theme={theme} previewMessage={previewMessage} />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { ReactBubbleInstructions } from './ReactBubbleInstructions'
|
||||
import { ReactPopupInstructions } from './ReactPopupInstructions'
|
||||
import { ReactStandardInstructions } from './ReactStandardInstructions'
|
||||
|
||||
type Props = {
|
||||
type: 'standard' | 'popup' | 'bubble'
|
||||
}
|
||||
|
||||
export const ReactInstructions = ({ type }: Props) => {
|
||||
switch (type) {
|
||||
case 'standard': {
|
||||
return <ReactStandardInstructions />
|
||||
}
|
||||
case 'popup': {
|
||||
return <ReactPopupInstructions />
|
||||
}
|
||||
case 'bubble': {
|
||||
return <ReactBubbleInstructions />
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { ListItem, OrderedList, Stack, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { PopupSettings } from '../../../settings/PopupSettings'
|
||||
import { InstallReactPackageSnippet } from '../InstallReactPackageSnippet'
|
||||
import { ReactPopupSnippet } from '../ReactPopupSnippet'
|
||||
|
||||
export const ReactPopupInstructions = () => {
|
||||
const [inputValue, setInputValue] = useState<number>()
|
||||
|
||||
return (
|
||||
<OrderedList spacing={4} pl={5}>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<Text>Install the packages</Text>
|
||||
<InstallReactPackageSnippet />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<PopupSettings
|
||||
onUpdateSettings={(settings) =>
|
||||
setInputValue(settings.autoShowDelay)
|
||||
}
|
||||
/>
|
||||
<ReactPopupSnippet autoShowDelay={inputValue} />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { ListItem, OrderedList, Stack, Text } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { StandardSettings } from '../../../settings/StandardSettings'
|
||||
import { InstallReactPackageSnippet } from '../InstallReactPackageSnippet'
|
||||
import { ReactStandardSnippet } from '../ReactStandardSnippet'
|
||||
|
||||
export const ReactStandardInstructions = () => {
|
||||
const [inputValues, setInputValues] = useState<{
|
||||
widthLabel?: string
|
||||
heightLabel: string
|
||||
}>({
|
||||
heightLabel: '100%',
|
||||
widthLabel: '100%',
|
||||
})
|
||||
|
||||
return (
|
||||
<OrderedList spacing={4} pl={5}>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<Text>Install the packages</Text>
|
||||
<InstallReactPackageSnippet />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Stack spacing={4}>
|
||||
<StandardSettings
|
||||
onUpdateWindowSettings={(settings) =>
|
||||
setInputValues({ ...settings })
|
||||
}
|
||||
/>
|
||||
<ReactStandardSnippet {...inputValues} />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
import { OrderedList, ListItem, Tag } from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { BubbleParams } from 'typebot-js'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
import parserHtml from 'prettier/parser-html'
|
||||
import prettier from 'prettier/standalone'
|
||||
import { env, getViewerUrl } from 'utils'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { ChatEmbedCode } from '../../codeSnippets/Chat/EmbedCode'
|
||||
import { ChatEmbedSettings } from '../../codeSnippets/Chat/EmbedSettings'
|
||||
import { StandardEmbedWindowSettings } from '../../codeSnippets/Container/EmbedSettings'
|
||||
import {
|
||||
parseInitContainerCode,
|
||||
typebotJsHtml,
|
||||
} from '../../codeSnippets/params'
|
||||
import { PopupEmbedCode } from '../../codeSnippets/Popup/EmbedCode'
|
||||
import { PopupEmbedSettings } from '../../codeSnippets/Popup/EmbedSettings'
|
||||
|
||||
type ShopifyInstructionsProps = {
|
||||
type: 'standard' | 'popup' | 'bubble'
|
||||
publicId: string
|
||||
}
|
||||
|
||||
export const ShopifyInstructions = ({
|
||||
type,
|
||||
publicId,
|
||||
}: ShopifyInstructionsProps) => {
|
||||
switch (type) {
|
||||
case 'standard': {
|
||||
return <StandardInstructions publicId={publicId} />
|
||||
}
|
||||
case 'popup': {
|
||||
return <PopupInstructions />
|
||||
}
|
||||
case 'bubble': {
|
||||
return <BubbleInstructions />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const StandardInstructions = ({ publicId }: Pick<ModalProps, 'publicId'>) => {
|
||||
const [windowSizes, setWindowSizes] = useState({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const jsCode = parseInitContainerCode({
|
||||
url: `${env('VIEWER_INTERNAL_URL') ?? getViewerUrl()}/${publicId}`,
|
||||
})
|
||||
const headCode = prettier.format(
|
||||
`${typebotJsHtml}<script>${jsCode}</script>`,
|
||||
{
|
||||
parser: 'html',
|
||||
plugins: [parserHtml],
|
||||
}
|
||||
)
|
||||
|
||||
const elementCode = prettier.format(
|
||||
`<div id="typebot-container" style="height: ${windowSizes.height}; width: ${windowSizes.width}"></div>`,
|
||||
{
|
||||
parser: 'html',
|
||||
plugins: [parserHtml],
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<OrderedList spacing={2} mb={4}>
|
||||
<ListItem>
|
||||
On your shop dashboard in the <Tag>Themes</Tag> page, click on{' '}
|
||||
<Tag>Actions {'>'} Edit code</Tag>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
In <Tag>Layout {'>'} theme.liquid</Tag> file, paste this code just
|
||||
before the closing <Tag>head</Tag> tag:
|
||||
<CodeEditor value={headCode} mt={2} lang="html" isReadOnly />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
Then, you can place an element on which the typebot will go in any file
|
||||
in the <Tag>body</Tag> tags. It needs to have the id{' '}
|
||||
<Tag>typebot-container</Tag>:
|
||||
<StandardEmbedWindowSettings
|
||||
my={4}
|
||||
onUpdateWindowSettings={(sizes) =>
|
||||
setWindowSizes({
|
||||
height: sizes.heightLabel,
|
||||
width: sizes.widthLabel,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<CodeEditor value={elementCode} mt={2} lang="html" isReadOnly />
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
||||
|
||||
const PopupInstructions = () => {
|
||||
const [inputValue, setInputValue] = useState<number>()
|
||||
|
||||
return (
|
||||
<OrderedList spacing={2} mb={4}>
|
||||
<ListItem>
|
||||
On your shop dashboard in the <Tag>Themes</Tag> page, click on{' '}
|
||||
<Tag>Actions {'>'} Edit code</Tag>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
In <Tag>Layout {'>'} theme.liquid</Tag> file, paste this code just
|
||||
before the closing <Tag>head</Tag> tag:
|
||||
<PopupEmbedSettings
|
||||
my="4"
|
||||
onUpdateSettings={(settings) => setInputValue(settings.delay)}
|
||||
/>
|
||||
<PopupEmbedCode delay={inputValue} />
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
||||
|
||||
const BubbleInstructions = () => {
|
||||
const [inputValues, setInputValues] = useState<
|
||||
Pick<BubbleParams, 'proactiveMessage' | 'button'>
|
||||
>({
|
||||
proactiveMessage: undefined,
|
||||
button: {
|
||||
color: '',
|
||||
iconUrl: '',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<OrderedList spacing={2} mb={4}>
|
||||
<ListItem>
|
||||
On your shop dashboard in the <Tag>Themes</Tag> page, click on{' '}
|
||||
<Tag>Actions {'>'} Edit code</Tag>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
In <Tag>Layout {'>'} theme.liquid</Tag> file, paste this code just
|
||||
before the closing <Tag>head</Tag> tag:
|
||||
<ChatEmbedSettings
|
||||
my="4"
|
||||
onUpdateSettings={(settings) => setInputValues({ ...settings })}
|
||||
/>
|
||||
<ChatEmbedCode mt={4} {...inputValues} />
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
)
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import React, { useState } from 'react'
|
||||
import { ModalProps } from '../../EmbedButton'
|
||||
import { EmbedModal } from '../../EmbedModal'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
import { ShopifyInstructions } from './instructions/ShopifyInstructions'
|
||||
|
||||
export const ShopifyModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
isPublished,
|
||||
publicId,
|
||||
}: ModalProps) => {
|
||||
const [selectedEmbedType, setSelectedEmbedType] = useState<
|
||||
'standard' | 'popup' | 'bubble' | undefined
|
||||
>()
|
||||
return (
|
||||
<EmbedModal
|
||||
titlePrefix="Shopify"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
isPublished={isPublished}
|
||||
onSelectEmbedType={setSelectedEmbedType}
|
||||
selectedEmbedType={selectedEmbedType}
|
||||
>
|
||||
{isDefined(selectedEmbedType) && (
|
||||
<ShopifyInstructions type={selectedEmbedType} publicId={publicId} />
|
||||
)}
|
||||
</EmbedModal>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './ShopifyModal'
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user