feat(editor): ✨ Start preview from any block
This commit is contained in:
@ -451,3 +451,9 @@ export const CreditCardIcon = (props: IconProps) => (
|
||||
<line x1="1" y1="10" x2="23" y2="10"></line>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const PlayIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</Icon>
|
||||
)
|
||||
|
@ -21,7 +21,7 @@ import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||
|
||||
export const PreviewDrawer = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const { setRightPanel } = useEditor()
|
||||
const { setRightPanel, startPreviewAtBlock } = useEditor()
|
||||
const { setPreviewingEdge } = useGraph()
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [width, setWidth] = useState(500)
|
||||
@ -96,13 +96,14 @@ export const PreviewDrawer = () => {
|
||||
borderRadius={'lg'}
|
||||
h="full"
|
||||
w="full"
|
||||
key={restartKey}
|
||||
key={restartKey + (startPreviewAtBlock ?? '')}
|
||||
pointerEvents={isResizing ? 'none' : 'auto'}
|
||||
>
|
||||
<TypebotViewer
|
||||
typebot={publicTypebot}
|
||||
onNewBlockVisible={setPreviewingEdge}
|
||||
onNewLog={handleNewLog}
|
||||
startBlockId={startPreviewAtBlock}
|
||||
isPreview
|
||||
/>
|
||||
</Flex>
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
Editable,
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
IconButton,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
@ -16,6 +17,8 @@ import { BlockNodeContextMenu } from './BlockNodeContextMenu'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { setMultipleRefs } from 'services/utils'
|
||||
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
|
||||
import { PlayIcon } from 'assets/icons'
|
||||
import { RightPanel, useEditor } from 'contexts/EditorContext'
|
||||
|
||||
type Props = {
|
||||
block: Block
|
||||
@ -38,6 +41,7 @@ export const BlockNode = ({ block, blockIndex }: Props) => {
|
||||
const { setMouseOverBlock, mouseOverBlock } = useStepDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const { setRightPanel, setStartPreviewAtBlock } = useEditor()
|
||||
const isPreviewing =
|
||||
previewingEdge?.from.blockId === block.id ||
|
||||
(previewingEdge?.to.blockId === block.id &&
|
||||
@ -99,6 +103,12 @@ export const BlockNode = ({ block, blockIndex }: Props) => {
|
||||
setFocusedBlockId(block.id)
|
||||
setIsMouseDown(true)
|
||||
}
|
||||
|
||||
const startPreviewAtThisBlock = () => {
|
||||
setStartPreviewAtBlock(block.id)
|
||||
setRightPanel(RightPanel.PREVIEW)
|
||||
}
|
||||
|
||||
const onDragStop = () => setIsMouseDown(false)
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
@ -165,6 +175,16 @@ export const BlockNode = ({ block, blockIndex }: Props) => {
|
||||
isStartBlock={isStartBlock}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<PlayIcon />}
|
||||
aria-label={'Preview bot from this group'}
|
||||
pos="absolute"
|
||||
right={2}
|
||||
top={0}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={startPreviewAtThisBlock}
|
||||
/>
|
||||
</Stack>
|
||||
</DraggableCore>
|
||||
)}
|
||||
|
@ -23,7 +23,6 @@ export const headerHeight = 56
|
||||
|
||||
export const TypebotHeader = () => {
|
||||
const router = useRouter()
|
||||
const { rightPanel } = useEditor()
|
||||
const {
|
||||
typebot,
|
||||
updateOnBothTypebots,
|
||||
@ -35,13 +34,14 @@ export const TypebotHeader = () => {
|
||||
canRedo,
|
||||
isSavingLoading,
|
||||
} = useTypebot()
|
||||
const { setRightPanel } = useEditor()
|
||||
const { setRightPanel, rightPanel, setStartPreviewAtBlock } = useEditor()
|
||||
|
||||
const handleNameSubmit = (name: string) => updateOnBothTypebots({ name })
|
||||
|
||||
const handleChangeIcon = (icon: string) => updateTypebot({ icon })
|
||||
|
||||
const handlePreviewClick = async () => {
|
||||
setStartPreviewAtBlock(undefined)
|
||||
save().then()
|
||||
setRightPanel(RightPanel.PREVIEW)
|
||||
}
|
||||
|
@ -13,18 +13,23 @@ export enum RightPanel {
|
||||
const editorContext = createContext<{
|
||||
rightPanel?: RightPanel
|
||||
setRightPanel: Dispatch<SetStateAction<RightPanel | undefined>>
|
||||
startPreviewAtBlock: string | undefined
|
||||
setStartPreviewAtBlock: Dispatch<SetStateAction<string | undefined>>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const EditorContext = ({ children }: { children: ReactNode }) => {
|
||||
const [rightPanel, setRightPanel] = useState<RightPanel>()
|
||||
const [startPreviewAtBlock, setStartPreviewAtBlock] = useState<string>()
|
||||
|
||||
return (
|
||||
<editorContext.Provider
|
||||
value={{
|
||||
rightPanel,
|
||||
setRightPanel,
|
||||
startPreviewAtBlock,
|
||||
setStartPreviewAtBlock,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -440,9 +440,11 @@ const useLinkedTypebots = ({
|
||||
},
|
||||
Error
|
||||
>(
|
||||
typebotIds?.every((id) => typebotId === id)
|
||||
? undefined
|
||||
: `/api/typebots?${params}`,
|
||||
workspaceId
|
||||
? typebotIds?.every((id) => typebotId === id)
|
||||
? undefined
|
||||
: `/api/typebots?${params}`
|
||||
: null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { byId } from 'utils'
|
||||
import { byId, isNotEmpty } from 'utils'
|
||||
import { MemberInWorkspace, Plan, Workspace, WorkspaceRole } from 'db'
|
||||
import {
|
||||
createNewWorkspace,
|
||||
|
@ -0,0 +1 @@
|
||||
{"id":"cl3wo63la1004801amwsqzbof","createdAt":"2022-06-02T07:01:46.030Z","updatedAt":"2022-06-02T07:34:02.336Z","icon":null,"name":"My typebot","publishedTypebotId":null,"folderId":null,"blocks":[{"id":"cl3wo63l80000801ae4lxgvad","steps":[{"id":"cl3wo63l80001801a8u9g96sp","type":"start","label":"Start","blockId":"cl3wo63l80000801ae4lxgvad","outgoingEdgeId":"cl3wo83ha000j2e6gdrk1crro"}],"title":"Start","graphCoordinates":{"x":0,"y":0}},{"id":"cl3wo7ucc000g2e6gdus80qeb","graphCoordinates":{"x":355,"y":-13},"title":"Group #1","steps":[{"id":"cl3wo7uce000h2e6gr9r3b11k","blockId":"cl3wo7ucc000g2e6gdus80qeb","type":"text","content":{"html":"<div>Hello this is group 1</div>","richText":[{"type":"p","children":[{"text":"Hello this is group 1"}]}],"plainText":"Hello this is group 1"}},{"id":"cl3wo8047000i2e6glma69ddz","blockId":"cl3wo7ucc000g2e6gdus80qeb","type":"text","content":{"html":"<div>What's your name?</div>","richText":[{"type":"p","children":[{"text":"What's your name?"}]}],"plainText":"What's your name?"}},{"id":"cl3wo85e8000k2e6gdb8qk860","blockId":"cl3wo7ucc000g2e6gdus80qeb","type":"text input","options":{"isLong":false,"labels":{"button":"Send","placeholder":"Type your answer..."}}}]},{"id":"cl3wo87et000l2e6ga64ipat6","graphCoordinates":{"x":22,"y":260},"title":"Group #1 copy","steps":[{"id":"cl3wo87eu000m2e6g5h90qs9u","blockId":"cl3wo87et000l2e6ga64ipat6","type":"text","content":{"html":"<div>Hello this is group 2</div>","richText":[{"type":"p","children":[{"text":"Hello this is group 2"}]}],"plainText":"Hello this is group 2"}},{"id":"cl3wo87ev000n2e6gp7vn2z62","blockId":"cl3wo87et000l2e6ga64ipat6","type":"text","content":{"html":"<div>What's your name?</div>","richText":[{"type":"p","children":[{"text":"What's your name?"}]}],"plainText":"What's your name?"}},{"id":"cl3wo87ev000o2e6g71r3hvor","blockId":"cl3wo87et000l2e6ga64ipat6","type":"text input","options":{"isLong":false,"labels":{"button":"Send","placeholder":"Type your answer..."}}}]},{"id":"cl3wo8kfl000p2e6gszlvkub0","graphCoordinates":{"x":367,"y":294},"title":"Group #1 copy copy","steps":[{"id":"cl3wo8kfl000q2e6gci1itvj3","blockId":"cl3wo8kfl000p2e6gszlvkub0","type":"text","content":{"html":"<div>Hello this is group 3</div>","richText":[{"type":"p","children":[{"text":"Hello this is group 3"}]}],"plainText":"Hello this is group 3"}},{"id":"cl3wo8kfl000r2e6gx0lxwitf","blockId":"cl3wo8kfl000p2e6gszlvkub0","type":"text","content":{"html":"<div>What's your name?</div>","richText":[{"type":"p","children":[{"text":"What's your name?"}]}],"plainText":"What's your name?"}},{"id":"cl3wo8kfl000s2e6g6ckc9om4","blockId":"cl3wo8kfl000p2e6gszlvkub0","type":"text input","options":{"isLong":false,"labels":{"button":"Send","placeholder":"Type your answer..."}}}]}],"variables":[],"edges":[{"from":{"blockId":"cl3wo63l80000801ae4lxgvad","stepId":"cl3wo63l80001801a8u9g96sp"},"to":{"blockId":"cl3wo7ucc000g2e6gdus80qeb"},"id":"cl3wo83ha000j2e6gdrk1crro"}],"theme":{"chat":{"inputs":{"color":"#303235","backgroundColor":"#FFFFFF","placeholderColor":"#9095A0"},"buttons":{"color":"#FFFFFF","backgroundColor":"#0042DA"},"hostAvatar":{"url":"https://avatars.githubusercontent.com/u/16015833?v=4","isEnabled":true},"hostBubbles":{"color":"#303235","backgroundColor":"#F7F8FF"},"guestBubbles":{"color":"#FFFFFF","backgroundColor":"#FF8E21"}},"general":{"font":"Open Sans","background":{"type":"None"}}},"settings":{"general":{"isBrandingEnabled":true,"isInputPrefillEnabled":true,"isHideQueryParamsEnabled":true,"isNewResultOnRefreshEnabled":false},"metadata":{"description":"Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."},"typingEmulation":{"speed":300,"enabled":true,"maxDelay":1.5}},"publicId":null,"customDomain":null,"workspaceId":"cl3ncues300081a1as58wmkxz"}
|
@ -11,7 +11,8 @@
|
||||
{
|
||||
"name": "typebot-20-modal",
|
||||
"value": "hide"
|
||||
}
|
||||
},
|
||||
{ "name": "workspaceId", "value": "freeWorkspace" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -87,7 +87,7 @@ export const createUsers = async () => {
|
||||
role: WorkspaceRole.ADMIN,
|
||||
workspace: {
|
||||
create: {
|
||||
id: freeWorkspaceId,
|
||||
id: 'free',
|
||||
name: "Free user's workspace",
|
||||
plan: Plan.FREE,
|
||||
},
|
||||
@ -98,12 +98,15 @@ export const createUsers = async () => {
|
||||
})
|
||||
await prisma.workspace.create({
|
||||
data: {
|
||||
id: 'free',
|
||||
name: 'Free workspace',
|
||||
id: freeWorkspaceId,
|
||||
name: 'Free Shared Workspace',
|
||||
plan: Plan.FREE,
|
||||
members: {
|
||||
createMany: {
|
||||
data: [{ role: WorkspaceRole.ADMIN, userId: 'proUser' }],
|
||||
data: [
|
||||
{ role: WorkspaceRole.MEMBER, userId: 'proUser' },
|
||||
{ role: WorkspaceRole.ADMIN, userId: 'freeUser' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
import { defaultTextInputOptions, InputStepType } from 'models'
|
||||
import path from 'path'
|
||||
import cuid from 'cuid'
|
||||
import { typebotViewer } from '../services/selectorUtils'
|
||||
|
||||
test.describe.parallel('Editor', () => {
|
||||
test('Edges connection should work', async ({ page }) => {
|
||||
@ -151,4 +152,29 @@ test.describe.parallel('Editor', () => {
|
||||
await expect(page.locator('text="😍"')).toBeVisible()
|
||||
await expect(page.locator('text="My superb typebot"')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Preview from group should work', async ({ page }) => {
|
||||
const typebotId = cuid()
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../fixtures/typebots/editor/previewFromGroup.json'),
|
||||
{
|
||||
id: typebotId,
|
||||
}
|
||||
)
|
||||
|
||||
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"')
|
||||
).toBeVisible()
|
||||
await page.click('[aria-label="Preview bot from this group"] >> nth=2')
|
||||
await expect(
|
||||
typebotViewer(page).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"')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
@ -35,7 +35,7 @@ test('can switch between workspaces and access typebot', async ({ page }) => {
|
||||
await page.goto('/typebots')
|
||||
await expect(page.locator('text="Pro typebot"')).toBeVisible()
|
||||
await page.click("text=Pro user's workspace")
|
||||
await page.click('text=Shared workspace')
|
||||
await page.click('text="Shared workspace"')
|
||||
await expect(page.locator('text="Pro typebot"')).toBeHidden()
|
||||
await page.click('text="Shared typebot"')
|
||||
await expect(page.locator('text="Hey there"')).toBeVisible()
|
||||
@ -135,7 +135,7 @@ test('can manage members', async ({ page }) => {
|
||||
test("can't edit workspace as a member", async ({ page }) => {
|
||||
await page.goto('/typebots')
|
||||
await page.click("text=Pro user's workspace")
|
||||
await page.click('text=Shared workspace')
|
||||
await page.click('text="Shared workspace"')
|
||||
await page.click('text=Settings & Members')
|
||||
await expect(page.locator('text="Settings"')).toBeHidden()
|
||||
await page.click('text="Members"')
|
||||
|
@ -33,10 +33,13 @@ type ChatBlockProps = {
|
||||
startStepIndex: number
|
||||
blockTitle: string
|
||||
keepShowingHostAvatar: boolean
|
||||
onBlockEnd: (
|
||||
edgeId?: string,
|
||||
onBlockEnd: ({
|
||||
edgeId,
|
||||
updatedTypebot,
|
||||
}: {
|
||||
edgeId?: string
|
||||
updatedTypebot?: PublicTypebot | LinkedTypebot
|
||||
) => void
|
||||
}) => void
|
||||
}
|
||||
|
||||
type ChatDisplayChunk = { bubbles: BubbleStep[]; input?: InputStep }
|
||||
@ -129,7 +132,9 @@ export const ChatBlock = ({
|
||||
currentStep.type === LogicStepType.REDIRECT &&
|
||||
currentStep.options.isNewTab === false
|
||||
if (isRedirecting) return
|
||||
nextEdgeId ? onBlockEnd(nextEdgeId, linkedTypebot) : displayNextStep()
|
||||
nextEdgeId
|
||||
? onBlockEnd({ edgeId: nextEdgeId, updatedTypebot: linkedTypebot })
|
||||
: displayNextStep()
|
||||
}
|
||||
if (isIntegrationStep(currentStep)) {
|
||||
const nextEdgeId = await executeIntegration({
|
||||
@ -149,9 +154,10 @@ export const ChatBlock = ({
|
||||
resultId,
|
||||
},
|
||||
})
|
||||
nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()
|
||||
nextEdgeId ? onBlockEnd({ edgeId: nextEdgeId }) : displayNextStep()
|
||||
}
|
||||
if (currentStep.type === 'start') onBlockEnd(currentStep.outgoingEdgeId)
|
||||
if (currentStep.type === 'start')
|
||||
onBlockEnd({ edgeId: currentStep.outgoingEdgeId })
|
||||
}
|
||||
|
||||
const displayNextStep = (answerContent?: string, isRetry?: boolean) => {
|
||||
@ -175,14 +181,14 @@ export const ChatBlock = ({
|
||||
const nextEdgeId = currentStep.items.find(
|
||||
(i) => i.content === answerContent
|
||||
)?.outgoingEdgeId
|
||||
if (nextEdgeId) return onBlockEnd(nextEdgeId)
|
||||
if (nextEdgeId) return onBlockEnd({ edgeId: nextEdgeId })
|
||||
}
|
||||
|
||||
if (currentStep?.outgoingEdgeId || processedSteps.length === steps.length)
|
||||
return onBlockEnd(currentStep.outgoingEdgeId)
|
||||
return onBlockEnd({ edgeId: currentStep.outgoingEdgeId })
|
||||
}
|
||||
const nextStep = steps[processedSteps.length + startStepIndex]
|
||||
nextStep ? insertStepInStack(nextStep) : onBlockEnd()
|
||||
nextStep ? insertStepInStack(nextStep) : onBlockEnd({})
|
||||
}
|
||||
|
||||
const avatarSrc = typebot.theme.chat.hostAvatar?.url
|
||||
|
@ -13,12 +13,14 @@ import { ChatContext } from 'contexts/ChatContext'
|
||||
type Props = {
|
||||
theme: Theme
|
||||
predefinedVariables?: { [key: string]: string | undefined }
|
||||
startBlockId?: string
|
||||
onNewBlockVisible: (edge: Edge) => void
|
||||
onCompleted: () => void
|
||||
}
|
||||
export const ConversationContainer = ({
|
||||
theme,
|
||||
predefinedVariables,
|
||||
startBlockId,
|
||||
onNewBlockVisible,
|
||||
onCompleted,
|
||||
}: Props) => {
|
||||
@ -36,17 +38,35 @@ export const ConversationContainer = ({
|
||||
const bottomAnchor = useRef<HTMLDivElement | null>(null)
|
||||
const scrollableContainer = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const displayNextBlock = (
|
||||
edgeId?: string,
|
||||
const displayNextBlock = ({
|
||||
edgeId,
|
||||
updatedTypebot,
|
||||
blockId,
|
||||
}: {
|
||||
edgeId?: string
|
||||
blockId?: string
|
||||
updatedTypebot?: PublicTypebot | LinkedTypebot
|
||||
) => {
|
||||
}) => {
|
||||
const currentTypebot = updatedTypebot ?? typebot
|
||||
if (blockId) {
|
||||
const nextBlock = currentTypebot.blocks.find(byId(blockId))
|
||||
if (!nextBlock) return
|
||||
onNewBlockVisible({
|
||||
id: 'edgeId',
|
||||
from: { blockId: 'block', stepId: 'step' },
|
||||
to: { blockId },
|
||||
})
|
||||
return setDisplayedBlocks([
|
||||
...displayedBlocks,
|
||||
{ block: nextBlock, startStepIndex: 0 },
|
||||
])
|
||||
}
|
||||
const nextEdge = currentTypebot.edges.find(byId(edgeId))
|
||||
if (!nextEdge) {
|
||||
if (linkedBotQueue.length > 0) {
|
||||
const nextEdgeId = linkedBotQueue[0].edgeId
|
||||
popEdgeIdFromLinkedTypebotQueue()
|
||||
displayNextBlock(nextEdgeId)
|
||||
displayNextBlock({ edgeId: nextEdgeId })
|
||||
}
|
||||
return onCompleted()
|
||||
}
|
||||
@ -65,7 +85,12 @@ export const ConversationContainer = ({
|
||||
useEffect(() => {
|
||||
const prefilledVariables = injectPredefinedVariables(predefinedVariables)
|
||||
updateVariables(prefilledVariables)
|
||||
displayNextBlock(typebot.blocks[0].steps[0].outgoingEdgeId)
|
||||
displayNextBlock({
|
||||
edgeId: startBlockId
|
||||
? undefined
|
||||
: typebot.blocks[0].steps[0].outgoingEdgeId,
|
||||
blockId: startBlockId,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
|
@ -30,6 +30,7 @@ export type TypebotViewerProps = {
|
||||
style?: CSSProperties
|
||||
predefinedVariables?: { [key: string]: string | undefined }
|
||||
resultId?: string
|
||||
startBlockId?: string
|
||||
onNewBlockVisible?: (edge: Edge) => void
|
||||
onNewAnswer?: (answer: Answer) => Promise<void>
|
||||
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||
@ -43,6 +44,7 @@ export const TypebotViewer = ({
|
||||
isPreview = false,
|
||||
style,
|
||||
resultId,
|
||||
startBlockId,
|
||||
predefinedVariables,
|
||||
onNewLog,
|
||||
onNewBlockVisible,
|
||||
@ -116,6 +118,7 @@ export const TypebotViewer = ({
|
||||
onNewBlockVisible={handleNewBlockVisible}
|
||||
onCompleted={handleCompleted}
|
||||
predefinedVariables={predefinedVariables}
|
||||
startBlockId={startBlockId}
|
||||
/>
|
||||
</div>
|
||||
{typebot.settings.general.isBrandingEnabled && <LiteBadge />}
|
||||
|
Reference in New Issue
Block a user