2
0

feat(editor): Start preview from any block

This commit is contained in:
Baptiste Arnaud
2022-06-02 10:07:50 +02:00
parent 12f2e40152
commit 89d91f9114
15 changed files with 128 additions and 29 deletions

View File

@ -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>
)

View File

@ -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>

View File

@ -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>
)}

View File

@ -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)
}

View File

@ -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}

View File

@ -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)

View File

@ -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,

View File

@ -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&#x27;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&#x27;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&#x27;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"}

View File

@ -11,7 +11,8 @@
{
"name": "typebot-20-modal",
"value": "hide"
}
},
{ "name": "workspaceId", "value": "freeWorkspace" }
]
}
]

View File

@ -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' },
],
},
},
},

View File

@ -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()
})
})

View File

@ -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"')

View File

@ -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

View File

@ -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
}, [])

View File

@ -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 />}