feat(viewer): ✨ Custom head code
This commit is contained in:
@@ -7,9 +7,15 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Image,
|
Image,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
|
Tooltip,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||||
import { Input, Textarea } from 'components/shared/Textbox'
|
import { Input, Textarea } from 'components/shared/Textbox'
|
||||||
|
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||||
|
import { HelpCircleIcon } from 'assets/icons'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebotName: string
|
typebotName: string
|
||||||
@@ -30,6 +36,8 @@ export const MetadataForm = ({
|
|||||||
onMetadataChange({ ...metadata, favIconUrl })
|
onMetadataChange({ ...metadata, favIconUrl })
|
||||||
const handleImageSubmit = (imageUrl: string) =>
|
const handleImageSubmit = (imageUrl: string) =>
|
||||||
onMetadataChange({ ...metadata, imageUrl })
|
onMetadataChange({ ...metadata, imageUrl })
|
||||||
|
const handleHeadCodeChange = (customHeadCode: string) =>
|
||||||
|
onMetadataChange({ ...metadata, customHeadCode })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing="6">
|
<Stack spacing="6">
|
||||||
@@ -37,7 +45,7 @@ export const MetadataForm = ({
|
|||||||
<FormLabel mb="0" htmlFor="icon">
|
<FormLabel mb="0" htmlFor="icon">
|
||||||
Icon:
|
Icon:
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Popover isLazy>
|
<Popover isLazy placement="top">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Image
|
<Image
|
||||||
src={metadata.favIconUrl ?? '/favicon.png'}
|
src={metadata.favIconUrl ?? '/favicon.png'}
|
||||||
@@ -62,7 +70,7 @@ export const MetadataForm = ({
|
|||||||
<FormLabel mb="0" htmlFor="image">
|
<FormLabel mb="0" htmlFor="image">
|
||||||
Image:
|
Image:
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Popover isLazy>
|
<Popover isLazy placement="top">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Image
|
<Image
|
||||||
src={metadata.imageUrl ?? '/viewer-preview.png'}
|
src={metadata.imageUrl ?? '/viewer-preview.png'}
|
||||||
@@ -102,6 +110,29 @@ export const MetadataForm = ({
|
|||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<HStack as={FormLabel} mb="0" htmlFor="head">
|
||||||
|
<Text>Custom head code:</Text>
|
||||||
|
<Tooltip
|
||||||
|
label={
|
||||||
|
'Will be pasted at the bottom of the header section, just above the closing head tag. Only `meta` and `script` tags are allowed.'
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
hasArrow
|
||||||
|
>
|
||||||
|
<Flex cursor="pointer">
|
||||||
|
<HelpCircleIcon />
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
|
<CodeEditor
|
||||||
|
id="head"
|
||||||
|
value={metadata.customHeadCode ?? ''}
|
||||||
|
onChange={handleHeadCodeChange}
|
||||||
|
lang="html"
|
||||||
|
withVariableButton={false}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ test.describe.parallel('Settings page', () => {
|
|||||||
await page.click('button:has-text("Metadata")')
|
await page.click('button:has-text("Metadata")')
|
||||||
|
|
||||||
// Fav icon
|
// Fav icon
|
||||||
const favIconImg = page.locator(':nth-match(img, 1)')
|
const favIconImg = page.locator('img >> nth=0')
|
||||||
await expect(favIconImg).toHaveAttribute('src', '/favicon.png')
|
await expect(favIconImg).toHaveAttribute('src', '/favicon.png')
|
||||||
await favIconImg.click()
|
await favIconImg.click()
|
||||||
await expect(page.locator('text=Giphy')).toBeHidden()
|
await expect(page.locator('text=Giphy')).toBeHidden()
|
||||||
@@ -94,11 +94,11 @@ test.describe.parallel('Settings page', () => {
|
|||||||
await expect(favIconImg).toHaveAttribute('src', favIconUrl)
|
await expect(favIconImg).toHaveAttribute('src', favIconUrl)
|
||||||
|
|
||||||
// Website image
|
// Website image
|
||||||
const websiteImg = page.locator(':nth-match(img, 2)')
|
const websiteImg = page.locator('img >> nth=1')
|
||||||
await expect(websiteImg).toHaveAttribute('src', '/viewer-preview.png')
|
await expect(websiteImg).toHaveAttribute('src', '/viewer-preview.png')
|
||||||
await websiteImg.click({ position: { x: 0, y: 180 } })
|
await websiteImg.click({ position: { x: 0, y: 160 }, force: true })
|
||||||
await expect(page.locator('text=Giphy')).toBeHidden()
|
await expect(page.locator('text=Giphy')).toBeHidden()
|
||||||
await page.click('button:has-text("Embed link")')
|
await page.click('button >> text="Embed link"')
|
||||||
await page.fill('input[placeholder="Paste the image link..."]', imageUrl)
|
await page.fill('input[placeholder="Paste the image link..."]', imageUrl)
|
||||||
await expect(websiteImg).toHaveAttribute('src', imageUrl)
|
await expect(websiteImg).toHaveAttribute('src', imageUrl)
|
||||||
|
|
||||||
@@ -107,6 +107,12 @@ test.describe.parallel('Settings page', () => {
|
|||||||
|
|
||||||
// Description
|
// Description
|
||||||
await page.fill('textarea#description', 'Lorem ipsum')
|
await page.fill('textarea#description', 'Lorem ipsum')
|
||||||
|
|
||||||
|
// Custom head code
|
||||||
|
await page.fill(
|
||||||
|
'div[contenteditable=true]',
|
||||||
|
'<script>Lorem ipsum</script>'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { Answer, PublicTypebot, VariableWithValue } from 'models'
|
|||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { upsertAnswer } from 'services/answer'
|
import { upsertAnswer } from 'services/answer'
|
||||||
|
import { isDefined } from 'utils'
|
||||||
import { SEO } from '../components/Seo'
|
import { SEO } from '../components/Seo'
|
||||||
import { createResult, updateResult } from '../services/result'
|
import { createResult, updateResult } from '../services/result'
|
||||||
import { ErrorPage } from './ErrorPage'
|
import { ErrorPage } from './ErrorPage'
|
||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
export type TypebotPageProps = {
|
export type TypebotPageProps = {
|
||||||
typebot?: PublicTypebot
|
typebot?: PublicTypebot
|
||||||
@@ -39,6 +41,13 @@ export const TypebotPage = ({
|
|||||||
})
|
})
|
||||||
setPredefinedVariables(predefinedVariables)
|
setPredefinedVariables(predefinedVariables)
|
||||||
initializeResult().then()
|
initializeResult().then()
|
||||||
|
const { customHeadCode } = typebot.settings.metadata
|
||||||
|
if (isDefined(customHeadCode) && customHeadCode !== '')
|
||||||
|
document.head.innerHTML =
|
||||||
|
document.head.innerHTML +
|
||||||
|
sanitizeHtml(customHeadCode ?? '', {
|
||||||
|
allowedTags: ['script', 'meta'],
|
||||||
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,10 @@
|
|||||||
"next": "^12.1.4",
|
"next": "^12.1.4",
|
||||||
"nodemailer": "^6.7.3",
|
"nodemailer": "^6.7.3",
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.10.3",
|
||||||
"utils": "*",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2",
|
||||||
|
"sanitize-html": "^2.7.0",
|
||||||
|
"utils": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.20.2",
|
"@playwright/test": "^1.20.2",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"@types/nodemailer": "^6.4.4",
|
"@types/nodemailer": "^6.4.4",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "^17.0.43",
|
"@types/react": "^17.0.43",
|
||||||
|
"@types/sanitize-html": "^2.6.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.17.0",
|
"@typescript-eslint/eslint-plugin": "^5.17.0",
|
||||||
"eslint": "<8.0.0",
|
"eslint": "<8.0.0",
|
||||||
"eslint-config-next": "12.1.4",
|
"eslint-config-next": "12.1.4",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type Metadata = {
|
|||||||
description: string
|
description: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
favIconUrl?: string
|
favIconUrl?: string
|
||||||
|
customHeadCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSettings: Settings = {
|
export const defaultSettings: Settings = {
|
||||||
|
|||||||
26
yarn.lock
26
yarn.lock
@@ -4734,6 +4734,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065"
|
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065"
|
||||||
integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==
|
integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==
|
||||||
|
|
||||||
|
"@types/sanitize-html@^2.6.2":
|
||||||
|
version "2.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.6.2.tgz#9c47960841b9def1e4c9dfebaaab010a3f6e97b9"
|
||||||
|
integrity sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ==
|
||||||
|
dependencies:
|
||||||
|
htmlparser2 "^6.0.0"
|
||||||
|
|
||||||
"@types/sax@^1.2.1":
|
"@types/sax@^1.2.1":
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.4.tgz#8221affa7f4f3cb21abd22f244cfabfa63e6a69e"
|
resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.4.tgz#8221affa7f4f3cb21abd22f244cfabfa63e6a69e"
|
||||||
@@ -9279,7 +9286,7 @@ htmlparser2@^3.9.1:
|
|||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
readable-stream "^3.1.1"
|
readable-stream "^3.1.1"
|
||||||
|
|
||||||
htmlparser2@^6.1.0:
|
htmlparser2@^6.0.0, htmlparser2@^6.1.0:
|
||||||
version "6.1.0"
|
version "6.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
|
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
|
||||||
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
|
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
|
||||||
@@ -12078,6 +12085,11 @@ parse-numeric-range@^1.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3"
|
resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3"
|
||||||
integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==
|
integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==
|
||||||
|
|
||||||
|
parse-srcset@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
||||||
|
integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@^6.0.1:
|
parse5-htmlparser2-tree-adapter@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
||||||
@@ -13801,6 +13813,18 @@ safe-identifier@^0.4.2:
|
|||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
|
sanitize-html@^2.7.0:
|
||||||
|
version "2.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.7.0.tgz#e106205b468aca932e2f9baf241f24660d34e279"
|
||||||
|
integrity sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA==
|
||||||
|
dependencies:
|
||||||
|
deepmerge "^4.2.2"
|
||||||
|
escape-string-regexp "^4.0.0"
|
||||||
|
htmlparser2 "^6.0.0"
|
||||||
|
is-plain-object "^5.0.0"
|
||||||
|
parse-srcset "^1.0.2"
|
||||||
|
postcss "^8.3.11"
|
||||||
|
|
||||||
sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
|
sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
|
|||||||
Reference in New Issue
Block a user