✨ (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) => {
|
||||
if (!workspace?.id) return
|
||||
console.log(icon)
|
||||
updateWorkspace(workspace?.id, { icon })
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
blockWidth,
|
||||
Coordinates,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FormLabel, Stack, Text } from '@chakra-ui/react'
|
||||
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { Input } from 'components/shared/Textbox'
|
||||
import { CodeOptions } from 'models'
|
||||
import React from 'react'
|
||||
@ -14,6 +15,10 @@ export const CodeSettings = ({ options, onOptionsChange }: Props) => {
|
||||
onOptionsChange({ ...options, name })
|
||||
const handleCodeChange = (content: string) =>
|
||||
onOptionsChange({ ...options, content })
|
||||
const handleShouldExecuteInParentContextChange = (
|
||||
shouldExecuteInParentContext: boolean
|
||||
) => onOptionsChange({ ...options, shouldExecuteInParentContext })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
@ -27,6 +32,13 @@ export const CodeSettings = ({ options, onOptionsChange }: Props) => {
|
||||
withVariableButton={false}
|
||||
/>
|
||||
</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>
|
||||
<Text>Code:</Text>
|
||||
<CodeEditor
|
||||
|
@ -80,6 +80,7 @@ test('can update workspace info', async ({ page }) => {
|
||||
await page.click('[data-testid="editable-icon"]')
|
||||
await page.fill('input[placeholder="Search..."]', 'building')
|
||||
await page.click('text="🏦"')
|
||||
await page.waitForTimeout(500)
|
||||
await page.fill('input[value="Pro workspace"]', 'My awesome workspace')
|
||||
await page.getByTestId('typebot-logo').click({ force: true })
|
||||
await expect(
|
||||
|
@ -17,7 +17,6 @@ import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
chatsLimit,
|
||||
computePrice,
|
||||
formatPrice,
|
||||
parseNumberWithCommas,
|
||||
storageLimit,
|
||||
} from 'utils'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Link, { LinkProps } from 'next/link'
|
||||
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'
|
||||
|
||||
type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean }
|
||||
|
@ -8,13 +8,13 @@ import { typebotViewer } from 'utils/playwright/testHelpers'
|
||||
|
||||
const mockSmtpCredentials: SmtpCredentialsData = {
|
||||
from: {
|
||||
email: 'sedrick.konopelski@ethereal.email',
|
||||
name: 'Kimberly Boyer',
|
||||
email: 'marley.cummings@ethereal.email',
|
||||
name: 'Marley Cummings',
|
||||
},
|
||||
host: 'smtp.ethereal.email',
|
||||
port: 587,
|
||||
username: 'sedrick.konopelski@ethereal.email',
|
||||
password: 'yXZChpPy25Qa5yBbeH',
|
||||
username: 'marley.cummings@ethereal.email',
|
||||
password: 'E5W1jHbAmv5cXXcut2',
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
@ -42,7 +42,9 @@ test('should send an email', async ({ page }) => {
|
||||
const { previewUrl } = await response.json()
|
||||
await page.goto(previewUrl)
|
||||
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="<test2@gmail.com>" >> nth=0')).toBeVisible()
|
||||
await expect(
|
||||
|
@ -45,7 +45,8 @@
|
||||
"react-dom": "18.2.0",
|
||||
"tailwindcss": "3.2.1",
|
||||
"typescript": "4.8.4",
|
||||
"utils": "workspace:*"
|
||||
"utils": "workspace:*",
|
||||
"typebot-js": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"db": "workspace:*",
|
||||
|
@ -8,6 +8,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { sendEventToParent } from 'services/chat'
|
||||
import { safeStringify } from 'services/variable'
|
||||
|
||||
export type LinkedTypebot = Pick<
|
||||
@ -75,6 +76,15 @@ export const TypebotContext = ({
|
||||
const updateVariableValue = (variableId: string, value: unknown) => {
|
||||
const formattedValue = safeStringify(value)
|
||||
|
||||
sendEventToParent({
|
||||
newVariableValue: {
|
||||
name:
|
||||
typebot.variables.find((variable) => variable.id === variableId)
|
||||
?.name ?? '',
|
||||
value: formattedValue ?? '',
|
||||
},
|
||||
})
|
||||
|
||||
setLocalTypebot((typebot) => ({
|
||||
...typebot,
|
||||
variables: typebot.variables.map((v) =>
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
TypingEmulation,
|
||||
} from 'models'
|
||||
import { isBubbleBlock, isInputBlock } from 'utils'
|
||||
import type { TypebotPostMessageData } from 'typebot-js'
|
||||
|
||||
export const computeTypingTimeout = (
|
||||
bubbleContent: string,
|
||||
@ -31,3 +32,17 @@ export const getLastChatBlockType = (
|
||||
) as (BubbleBlock | InputBlock)[]
|
||||
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,
|
||||
} from 'models'
|
||||
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'
|
||||
|
||||
type EdgeId = string
|
||||
@ -153,14 +154,7 @@ const executeRedirect = (
|
||||
try {
|
||||
window.open(formattedUrl)
|
||||
} catch (err) {
|
||||
//Can't access to parent window
|
||||
window.top?.postMessage(
|
||||
{
|
||||
from: 'typebot',
|
||||
redirectUrl: formattedUrl,
|
||||
},
|
||||
'*'
|
||||
)
|
||||
sendEventToParent({ redirectUrl: formattedUrl })
|
||||
}
|
||||
} else {
|
||||
window.open(formattedUrl, block.options.isNewTab ? '_blank' : '_self')
|
||||
@ -173,15 +167,25 @@ const executeCode = async (
|
||||
{ typebot: { variables } }: LogicContext
|
||||
) => {
|
||||
if (!block.options.content) return
|
||||
const func = Function(
|
||||
...variables.map((v) => v.id),
|
||||
parseVariables(variables, { fieldToParse: 'id' })(block.options.content)
|
||||
)
|
||||
try {
|
||||
await func(...variables.map((v) => parseCorrectValueType(v.value)))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
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(
|
||||
...variables.map((v) => v.id),
|
||||
parseVariables(variables, { fieldToParse: 'id' })(block.options.content)
|
||||
)
|
||||
try {
|
||||
await func(...variables.map((v) => parseCorrectValueType(v.value)))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return block.outgoingEdgeId
|
||||
}
|
||||
|
||||
|
@ -9,3 +9,8 @@ export const sanitizeUrl = (url: string): string =>
|
||||
export const isMobile =
|
||||
typeof window !== 'undefined' &&
|
||||
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({
|
||||
name: z.string(),
|
||||
content: z.string().optional(),
|
||||
shouldExecuteInParentContext: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const codeBlockSchema = blockBaseSchema.and(
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "typebot-js",
|
||||
"version": "2.2.9",
|
||||
"version": "2.2.10",
|
||||
"main": "dist/index.js",
|
||||
"unpkg": "dist/index.umd.min.js",
|
||||
"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'
|
||||
|
||||
export const createIframe = ({
|
||||
@ -24,8 +24,8 @@ export const createIframe = ({
|
||||
iframe.setAttribute('id', uniqueId)
|
||||
if (backgroundColor) iframe.style.backgroundColor = backgroundColor
|
||||
iframe.classList.add('typebot-iframe')
|
||||
const { onNewVariableValue, onVideoPlayed } = iframeParams
|
||||
listenForTypebotMessages({ onNewVariableValue, onVideoPlayed })
|
||||
const { onNewVariableValue } = iframeParams
|
||||
listenForTypebotMessages({ onNewVariableValue })
|
||||
return iframe
|
||||
}
|
||||
|
||||
@ -50,14 +50,17 @@ const parseStarterVariables = (starterVariables?: {
|
||||
|
||||
export const listenForTypebotMessages = (callbacks: IframeCallbacks) => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
const processMessage = (data: DataFromTypebot, callbacks: IframeCallbacks) => {
|
||||
const processMessage = (
|
||||
data: TypebotPostMessageData,
|
||||
callbacks: IframeCallbacks
|
||||
) => {
|
||||
if (data.redirectUrl) window.open(data.redirectUrl)
|
||||
if (data.newVariableValue && callbacks.onNewVariableValue)
|
||||
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 = {
|
||||
onNewVariableValue?: (v: Variable) => void
|
||||
onVideoPlayed?: () => void
|
||||
}
|
||||
|
||||
export type PopupParams = {
|
||||
@ -52,10 +51,10 @@ export type Variable = {
|
||||
value: string
|
||||
}
|
||||
|
||||
export type DataFromTypebot = {
|
||||
export type TypebotPostMessageData = {
|
||||
redirectUrl?: string
|
||||
newVariableValue?: Variable
|
||||
videoPlayed?: boolean
|
||||
codeToExecute?: Function
|
||||
}
|
||||
|
||||
export const localStorageKeys = {
|
||||
|
@ -88,26 +88,6 @@ describe('createIframe', () => {
|
||||
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 () => {
|
||||
expect.assertions(3)
|
||||
let n, v
|
||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -441,6 +441,7 @@ importers:
|
||||
react-transition-group: 4.4.5
|
||||
resize-observer: 1.0.4
|
||||
tailwindcss: 3.2.1
|
||||
typebot-js: workspace:*
|
||||
typescript: 4.8.4
|
||||
utils: workspace:*
|
||||
dependencies:
|
||||
@ -476,6 +477,7 @@ importers:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
tailwindcss: 3.2.1_postcss@8.4.18
|
||||
typebot-js: link:../typebot-js
|
||||
typescript: 4.8.4
|
||||
utils: link:../utils
|
||||
|
||||
|
Reference in New Issue
Block a user