✨ (logic) Add execute in parent window context for code block
This commit is contained in:
@ -24,6 +24,7 @@ export const WorkspaceSettingsForm = ({ onClose }: { onClose: () => void }) => {
|
|||||||
|
|
||||||
const handleChangeIcon = (icon: string) => {
|
const handleChangeIcon = (icon: string) => {
|
||||||
if (!workspace?.id) return
|
if (!workspace?.id) return
|
||||||
|
console.log(icon)
|
||||||
updateWorkspace(workspace?.id, { icon })
|
updateWorkspace(workspace?.id, { icon })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||||
import React, { useRef, useMemo, useEffect, useState, useCallback } from 'react'
|
import React, { useRef, useMemo, useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
blockWidth,
|
blockWidth,
|
||||||
Coordinates,
|
Coordinates,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FormLabel, Stack, Text } from '@chakra-ui/react'
|
import { FormLabel, Stack, Text } from '@chakra-ui/react'
|
||||||
import { CodeEditor } from 'components/shared/CodeEditor'
|
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||||
|
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||||
import { Input } from 'components/shared/Textbox'
|
import { Input } from 'components/shared/Textbox'
|
||||||
import { CodeOptions } from 'models'
|
import { CodeOptions } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@ -14,6 +15,10 @@ export const CodeSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
onOptionsChange({ ...options, name })
|
onOptionsChange({ ...options, name })
|
||||||
const handleCodeChange = (content: string) =>
|
const handleCodeChange = (content: string) =>
|
||||||
onOptionsChange({ ...options, content })
|
onOptionsChange({ ...options, content })
|
||||||
|
const handleShouldExecuteInParentContextChange = (
|
||||||
|
shouldExecuteInParentContext: boolean
|
||||||
|
) => onOptionsChange({ ...options, shouldExecuteInParentContext })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Stack>
|
<Stack>
|
||||||
@ -27,6 +32,13 @@ export const CodeSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
withVariableButton={false}
|
withVariableButton={false}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<SwitchWithLabel
|
||||||
|
id="shouldExecuteInParentContext"
|
||||||
|
label="Execute in parent window"
|
||||||
|
moreInfoContent="Execute the code in the parent window context (when the bot is embedded). If it isn't detected, the code will be executed in the current window context."
|
||||||
|
initialValue={options.shouldExecuteInParentContext ?? false}
|
||||||
|
onCheckChange={handleShouldExecuteInParentContextChange}
|
||||||
|
/>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>Code:</Text>
|
<Text>Code:</Text>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
@ -80,6 +80,7 @@ test('can update workspace info', async ({ page }) => {
|
|||||||
await page.click('[data-testid="editable-icon"]')
|
await page.click('[data-testid="editable-icon"]')
|
||||||
await page.fill('input[placeholder="Search..."]', 'building')
|
await page.fill('input[placeholder="Search..."]', 'building')
|
||||||
await page.click('text="🏦"')
|
await page.click('text="🏦"')
|
||||||
|
await page.waitForTimeout(500)
|
||||||
await page.fill('input[value="Pro workspace"]', 'My awesome workspace')
|
await page.fill('input[value="Pro workspace"]', 'My awesome workspace')
|
||||||
await page.getByTestId('typebot-logo').click({ force: true })
|
await page.getByTestId('typebot-logo').click({ force: true })
|
||||||
await expect(
|
await expect(
|
||||||
|
@ -17,7 +17,6 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
chatsLimit,
|
chatsLimit,
|
||||||
computePrice,
|
computePrice,
|
||||||
formatPrice,
|
|
||||||
parseNumberWithCommas,
|
parseNumberWithCommas,
|
||||||
storageLimit,
|
storageLimit,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Link, { LinkProps } from 'next/link'
|
import Link, { LinkProps } from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Box, chakra, HStack, Stack, TextProps } from '@chakra-ui/react'
|
import { chakra, HStack, TextProps } from '@chakra-ui/react'
|
||||||
import { ExternalLinkIcon } from 'assets/icons/ExternalLinkIcon'
|
import { ExternalLinkIcon } from 'assets/icons/ExternalLinkIcon'
|
||||||
|
|
||||||
type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean }
|
type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean }
|
||||||
|
@ -8,13 +8,13 @@ import { typebotViewer } from 'utils/playwright/testHelpers'
|
|||||||
|
|
||||||
const mockSmtpCredentials: SmtpCredentialsData = {
|
const mockSmtpCredentials: SmtpCredentialsData = {
|
||||||
from: {
|
from: {
|
||||||
email: 'sedrick.konopelski@ethereal.email',
|
email: 'marley.cummings@ethereal.email',
|
||||||
name: 'Kimberly Boyer',
|
name: 'Marley Cummings',
|
||||||
},
|
},
|
||||||
host: 'smtp.ethereal.email',
|
host: 'smtp.ethereal.email',
|
||||||
port: 587,
|
port: 587,
|
||||||
username: 'sedrick.konopelski@ethereal.email',
|
username: 'marley.cummings@ethereal.email',
|
||||||
password: 'yXZChpPy25Qa5yBbeH',
|
password: 'E5W1jHbAmv5cXXcut2',
|
||||||
}
|
}
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
@ -42,7 +42,9 @@ test('should send an email', async ({ page }) => {
|
|||||||
const { previewUrl } = await response.json()
|
const { previewUrl } = await response.json()
|
||||||
await page.goto(previewUrl)
|
await page.goto(previewUrl)
|
||||||
await expect(page.locator('text="Hey!"')).toBeVisible()
|
await expect(page.locator('text="Hey!"')).toBeVisible()
|
||||||
await expect(page.locator('text="Kimberly Boyer"')).toBeVisible()
|
await expect(
|
||||||
|
page.locator(`text="${mockSmtpCredentials.from.name}"`)
|
||||||
|
).toBeVisible()
|
||||||
await expect(page.locator('text="<test1@gmail.com>" >> nth=0')).toBeVisible()
|
await expect(page.locator('text="<test1@gmail.com>" >> nth=0')).toBeVisible()
|
||||||
await expect(page.locator('text="<test2@gmail.com>" >> nth=0')).toBeVisible()
|
await expect(page.locator('text="<test2@gmail.com>" >> nth=0')).toBeVisible()
|
||||||
await expect(
|
await expect(
|
||||||
|
@ -45,7 +45,8 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"tailwindcss": "3.2.1",
|
"tailwindcss": "3.2.1",
|
||||||
"typescript": "4.8.4",
|
"typescript": "4.8.4",
|
||||||
"utils": "workspace:*"
|
"utils": "workspace:*",
|
||||||
|
"typebot-js": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"db": "workspace:*",
|
"db": "workspace:*",
|
||||||
|
@ -8,6 +8,7 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import { sendEventToParent } from 'services/chat'
|
||||||
import { safeStringify } from 'services/variable'
|
import { safeStringify } from 'services/variable'
|
||||||
|
|
||||||
export type LinkedTypebot = Pick<
|
export type LinkedTypebot = Pick<
|
||||||
@ -75,6 +76,15 @@ export const TypebotContext = ({
|
|||||||
const updateVariableValue = (variableId: string, value: unknown) => {
|
const updateVariableValue = (variableId: string, value: unknown) => {
|
||||||
const formattedValue = safeStringify(value)
|
const formattedValue = safeStringify(value)
|
||||||
|
|
||||||
|
sendEventToParent({
|
||||||
|
newVariableValue: {
|
||||||
|
name:
|
||||||
|
typebot.variables.find((variable) => variable.id === variableId)
|
||||||
|
?.name ?? '',
|
||||||
|
value: formattedValue ?? '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
setLocalTypebot((typebot) => ({
|
setLocalTypebot((typebot) => ({
|
||||||
...typebot,
|
...typebot,
|
||||||
variables: typebot.variables.map((v) =>
|
variables: typebot.variables.map((v) =>
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
TypingEmulation,
|
TypingEmulation,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { isBubbleBlock, isInputBlock } from 'utils'
|
import { isBubbleBlock, isInputBlock } from 'utils'
|
||||||
|
import type { TypebotPostMessageData } from 'typebot-js'
|
||||||
|
|
||||||
export const computeTypingTimeout = (
|
export const computeTypingTimeout = (
|
||||||
bubbleContent: string,
|
bubbleContent: string,
|
||||||
@ -31,3 +32,17 @@ export const getLastChatBlockType = (
|
|||||||
) as (BubbleBlock | InputBlock)[]
|
) as (BubbleBlock | InputBlock)[]
|
||||||
return displayedBlocks.pop()?.type
|
return displayedBlocks.pop()?.type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendEventToParent = (data: TypebotPostMessageData) => {
|
||||||
|
try {
|
||||||
|
window.top?.postMessage(
|
||||||
|
{
|
||||||
|
from: 'typebot',
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,7 +19,8 @@ import {
|
|||||||
VariableWithUnknowValue,
|
VariableWithUnknowValue,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { byId, isDefined, isNotDefined, sendRequest } from 'utils'
|
import { byId, isDefined, isNotDefined, sendRequest } from 'utils'
|
||||||
import { sanitizeUrl } from './utils'
|
import { sendEventToParent } from './chat'
|
||||||
|
import { isEmbedded, sanitizeUrl } from './utils'
|
||||||
import { parseCorrectValueType, parseVariables } from './variable'
|
import { parseCorrectValueType, parseVariables } from './variable'
|
||||||
|
|
||||||
type EdgeId = string
|
type EdgeId = string
|
||||||
@ -153,14 +154,7 @@ const executeRedirect = (
|
|||||||
try {
|
try {
|
||||||
window.open(formattedUrl)
|
window.open(formattedUrl)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//Can't access to parent window
|
sendEventToParent({ redirectUrl: formattedUrl })
|
||||||
window.top?.postMessage(
|
|
||||||
{
|
|
||||||
from: 'typebot',
|
|
||||||
redirectUrl: formattedUrl,
|
|
||||||
},
|
|
||||||
'*'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.open(formattedUrl, block.options.isNewTab ? '_blank' : '_self')
|
window.open(formattedUrl, block.options.isNewTab ? '_blank' : '_self')
|
||||||
@ -173,6 +167,14 @@ const executeCode = async (
|
|||||||
{ typebot: { variables } }: LogicContext
|
{ typebot: { variables } }: LogicContext
|
||||||
) => {
|
) => {
|
||||||
if (!block.options.content) return
|
if (!block.options.content) return
|
||||||
|
console.log('isEmbedded', isEmbedded)
|
||||||
|
if (block.options.shouldExecuteInParentContext && isEmbedded) {
|
||||||
|
const func = Function(
|
||||||
|
...variables.map((v) => v.id),
|
||||||
|
parseVariables(variables)(block.options.content)
|
||||||
|
)
|
||||||
|
sendEventToParent({ codeToExecute: func })
|
||||||
|
} else {
|
||||||
const func = Function(
|
const func = Function(
|
||||||
...variables.map((v) => v.id),
|
...variables.map((v) => v.id),
|
||||||
parseVariables(variables, { fieldToParse: 'id' })(block.options.content)
|
parseVariables(variables, { fieldToParse: 'id' })(block.options.content)
|
||||||
@ -182,6 +184,8 @@ const executeCode = async (
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return block.outgoingEdgeId
|
return block.outgoingEdgeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,3 +9,8 @@ export const sanitizeUrl = (url: string): string =>
|
|||||||
export const isMobile =
|
export const isMobile =
|
||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
window.matchMedia('only screen and (max-width: 760px)').matches
|
window.matchMedia('only screen and (max-width: 760px)').matches
|
||||||
|
|
||||||
|
export const isEmbedded =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.parent &&
|
||||||
|
window.location !== window.top?.location
|
||||||
|
@ -4,6 +4,7 @@ import { LogicBlockType, blockBaseSchema } from '../shared'
|
|||||||
export const codeOptionsSchema = z.object({
|
export const codeOptionsSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
|
shouldExecuteInParentContext: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const codeBlockSchema = blockBaseSchema.and(
|
export const codeBlockSchema = blockBaseSchema.and(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "typebot-js",
|
"name": "typebot-js",
|
||||||
"version": "2.2.9",
|
"version": "2.2.10",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"unpkg": "dist/index.umd.min.js",
|
"unpkg": "dist/index.umd.min.js",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DataFromTypebot, IframeCallbacks, IframeParams } from '../types'
|
import { TypebotPostMessageData, IframeCallbacks, IframeParams } from '../types'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
export const createIframe = ({
|
export const createIframe = ({
|
||||||
@ -24,8 +24,8 @@ export const createIframe = ({
|
|||||||
iframe.setAttribute('id', uniqueId)
|
iframe.setAttribute('id', uniqueId)
|
||||||
if (backgroundColor) iframe.style.backgroundColor = backgroundColor
|
if (backgroundColor) iframe.style.backgroundColor = backgroundColor
|
||||||
iframe.classList.add('typebot-iframe')
|
iframe.classList.add('typebot-iframe')
|
||||||
const { onNewVariableValue, onVideoPlayed } = iframeParams
|
const { onNewVariableValue } = iframeParams
|
||||||
listenForTypebotMessages({ onNewVariableValue, onVideoPlayed })
|
listenForTypebotMessages({ onNewVariableValue })
|
||||||
return iframe
|
return iframe
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,14 +50,17 @@ const parseStarterVariables = (starterVariables?: {
|
|||||||
|
|
||||||
export const listenForTypebotMessages = (callbacks: IframeCallbacks) => {
|
export const listenForTypebotMessages = (callbacks: IframeCallbacks) => {
|
||||||
window.addEventListener('message', (event) => {
|
window.addEventListener('message', (event) => {
|
||||||
const data = event.data as { from?: 'typebot' } & DataFromTypebot
|
const data = event.data as { from?: 'typebot' } & TypebotPostMessageData
|
||||||
if (data.from === 'typebot') processMessage(event.data, callbacks)
|
if (data.from === 'typebot') processMessage(event.data, callbacks)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const processMessage = (data: DataFromTypebot, callbacks: IframeCallbacks) => {
|
const processMessage = (
|
||||||
|
data: TypebotPostMessageData,
|
||||||
|
callbacks: IframeCallbacks
|
||||||
|
) => {
|
||||||
if (data.redirectUrl) window.open(data.redirectUrl)
|
if (data.redirectUrl) window.open(data.redirectUrl)
|
||||||
if (data.newVariableValue && callbacks.onNewVariableValue)
|
if (data.newVariableValue && callbacks.onNewVariableValue)
|
||||||
callbacks.onNewVariableValue(data.newVariableValue)
|
callbacks.onNewVariableValue(data.newVariableValue)
|
||||||
if (data.videoPlayed && callbacks.onVideoPlayed) callbacks.onVideoPlayed()
|
if (data.codeToExecute) data.codeToExecute()
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ export type IframeParams = {
|
|||||||
|
|
||||||
export type IframeCallbacks = {
|
export type IframeCallbacks = {
|
||||||
onNewVariableValue?: (v: Variable) => void
|
onNewVariableValue?: (v: Variable) => void
|
||||||
onVideoPlayed?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PopupParams = {
|
export type PopupParams = {
|
||||||
@ -52,10 +51,10 @@ export type Variable = {
|
|||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataFromTypebot = {
|
export type TypebotPostMessageData = {
|
||||||
redirectUrl?: string
|
redirectUrl?: string
|
||||||
newVariableValue?: Variable
|
newVariableValue?: Variable
|
||||||
videoPlayed?: boolean
|
codeToExecute?: Function
|
||||||
}
|
}
|
||||||
|
|
||||||
export const localStorageKeys = {
|
export const localStorageKeys = {
|
||||||
|
@ -88,26 +88,6 @@ describe('createIframe', () => {
|
|||||||
expect(v).toBe('varValue')
|
expect(v).toBe('varValue')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should notify when video played', async () => {
|
|
||||||
expect.assertions(1)
|
|
||||||
let hit = false
|
|
||||||
createIframe({
|
|
||||||
url: 'https://typebot.io/typebot-id',
|
|
||||||
onVideoPlayed: () => {
|
|
||||||
hit = true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
window.postMessage(
|
|
||||||
{
|
|
||||||
from: 'typebot',
|
|
||||||
videoPlayed: true,
|
|
||||||
},
|
|
||||||
'*'
|
|
||||||
)
|
|
||||||
await new Promise((r) => setTimeout(r, 1))
|
|
||||||
expect(hit).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("shouldn't execute callbacks if event from other than typebot", async () => {
|
it("shouldn't execute callbacks if event from other than typebot", async () => {
|
||||||
expect.assertions(3)
|
expect.assertions(3)
|
||||||
let n, v
|
let n, v
|
||||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -441,6 +441,7 @@ importers:
|
|||||||
react-transition-group: 4.4.5
|
react-transition-group: 4.4.5
|
||||||
resize-observer: 1.0.4
|
resize-observer: 1.0.4
|
||||||
tailwindcss: 3.2.1
|
tailwindcss: 3.2.1
|
||||||
|
typebot-js: workspace:*
|
||||||
typescript: 4.8.4
|
typescript: 4.8.4
|
||||||
utils: workspace:*
|
utils: workspace:*
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -476,6 +477,7 @@ importers:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
tailwindcss: 3.2.1_postcss@8.4.18
|
tailwindcss: 3.2.1_postcss@8.4.18
|
||||||
|
typebot-js: link:../typebot-js
|
||||||
typescript: 4.8.4
|
typescript: 4.8.4
|
||||||
utils: link:../utils
|
utils: link:../utils
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user