From 89d91f9114c0ce16cedd34ef8a2348c2c734571b Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 2 Jun 2022 10:07:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E2=9C=A8=20Start=20preview=20f?= =?UTF-8?q?rom=20any=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/builder/assets/icons.tsx | 6 ++++ .../editor/preview/PreviewDrawer.tsx | 5 +-- .../Graph/Nodes/BlockNode/BlockNode.tsx | 20 +++++++++++ .../shared/TypebotHeader/TypebotHeader.tsx | 4 +-- apps/builder/contexts/EditorContext.tsx | 5 +++ .../TypebotContext/TypebotContext.tsx | 8 +++-- apps/builder/contexts/WorkspaceContext.tsx | 2 +- .../typebots/editor/previewFromGroup.json | 1 + apps/builder/playwright/freeUser.json | 3 +- apps/builder/playwright/services/database.ts | 11 +++--- apps/builder/playwright/tests/editor.spec.ts | 26 ++++++++++++++ .../playwright/tests/workspaces.spec.ts | 4 +-- .../src/components/ChatBlock/ChatBlock.tsx | 24 ++++++++----- .../src/components/ConversationContainer.tsx | 35 ++++++++++++++++--- .../src/components/TypebotViewer.tsx | 3 ++ 15 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 apps/builder/playwright/fixtures/typebots/editor/previewFromGroup.json diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx index d5ce6a849..2768bf666 100644 --- a/apps/builder/assets/icons.tsx +++ b/apps/builder/assets/icons.tsx @@ -451,3 +451,9 @@ export const CreditCardIcon = (props: IconProps) => ( ) + +export const PlayIcon = (props: IconProps) => ( + + + +) diff --git a/apps/builder/components/editor/preview/PreviewDrawer.tsx b/apps/builder/components/editor/preview/PreviewDrawer.tsx index c0f67e1f9..11baaf420 100644 --- a/apps/builder/components/editor/preview/PreviewDrawer.tsx +++ b/apps/builder/components/editor/preview/PreviewDrawer.tsx @@ -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'} > diff --git a/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNode.tsx b/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNode.tsx index da2d35ebc..4a6930d80 100644 --- a/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNode.tsx +++ b/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNode.tsx @@ -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 ( @@ -165,6 +175,16 @@ export const BlockNode = ({ block, blockIndex }: Props) => { isStartBlock={isStartBlock} /> )} + } + aria-label={'Preview bot from this group'} + pos="absolute" + right={2} + top={0} + size="sm" + variant="outline" + onClick={startPreviewAtThisBlock} + /> )} diff --git a/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx b/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx index 3197bc904..ac67e47b3 100644 --- a/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx +++ b/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx @@ -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) } diff --git a/apps/builder/contexts/EditorContext.tsx b/apps/builder/contexts/EditorContext.tsx index 26d66de24..b774a6bbc 100644 --- a/apps/builder/contexts/EditorContext.tsx +++ b/apps/builder/contexts/EditorContext.tsx @@ -13,18 +13,23 @@ export enum RightPanel { const editorContext = createContext<{ rightPanel?: RightPanel setRightPanel: Dispatch> + startPreviewAtBlock: string | undefined + setStartPreviewAtBlock: Dispatch> // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore }>({}) export const EditorContext = ({ children }: { children: ReactNode }) => { const [rightPanel, setRightPanel] = useState() + const [startPreviewAtBlock, setStartPreviewAtBlock] = useState() return ( {children} diff --git a/apps/builder/contexts/TypebotContext/TypebotContext.tsx b/apps/builder/contexts/TypebotContext/TypebotContext.tsx index 6b8b4e7fd..2eb95d660 100644 --- a/apps/builder/contexts/TypebotContext/TypebotContext.tsx +++ b/apps/builder/contexts/TypebotContext/TypebotContext.tsx @@ -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) diff --git a/apps/builder/contexts/WorkspaceContext.tsx b/apps/builder/contexts/WorkspaceContext.tsx index ef759017e..03976f517 100644 --- a/apps/builder/contexts/WorkspaceContext.tsx +++ b/apps/builder/contexts/WorkspaceContext.tsx @@ -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, diff --git a/apps/builder/playwright/fixtures/typebots/editor/previewFromGroup.json b/apps/builder/playwright/fixtures/typebots/editor/previewFromGroup.json new file mode 100644 index 000000000..ad5e7173d --- /dev/null +++ b/apps/builder/playwright/fixtures/typebots/editor/previewFromGroup.json @@ -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":"
Hello this is group 1
","richText":[{"type":"p","children":[{"text":"Hello this is group 1"}]}],"plainText":"Hello this is group 1"}},{"id":"cl3wo8047000i2e6glma69ddz","blockId":"cl3wo7ucc000g2e6gdus80qeb","type":"text","content":{"html":"
What's your name?
","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":"
Hello this is group 2
","richText":[{"type":"p","children":[{"text":"Hello this is group 2"}]}],"plainText":"Hello this is group 2"}},{"id":"cl3wo87ev000n2e6gp7vn2z62","blockId":"cl3wo87et000l2e6ga64ipat6","type":"text","content":{"html":"
What's your name?
","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":"
Hello this is group 3
","richText":[{"type":"p","children":[{"text":"Hello this is group 3"}]}],"plainText":"Hello this is group 3"}},{"id":"cl3wo8kfl000r2e6gx0lxwitf","blockId":"cl3wo8kfl000p2e6gszlvkub0","type":"text","content":{"html":"
What's your name?
","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"} \ No newline at end of file diff --git a/apps/builder/playwright/freeUser.json b/apps/builder/playwright/freeUser.json index ea80f8004..c147b6fde 100644 --- a/apps/builder/playwright/freeUser.json +++ b/apps/builder/playwright/freeUser.json @@ -11,7 +11,8 @@ { "name": "typebot-20-modal", "value": "hide" - } + }, + { "name": "workspaceId", "value": "freeWorkspace" } ] } ] diff --git a/apps/builder/playwright/services/database.ts b/apps/builder/playwright/services/database.ts index 143e874b6..ec7b28ff0 100644 --- a/apps/builder/playwright/services/database.ts +++ b/apps/builder/playwright/services/database.ts @@ -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' }, + ], }, }, }, diff --git a/apps/builder/playwright/tests/editor.spec.ts b/apps/builder/playwright/tests/editor.spec.ts index e1d2e6117..aa696d32c 100644 --- a/apps/builder/playwright/tests/editor.spec.ts +++ b/apps/builder/playwright/tests/editor.spec.ts @@ -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() + }) }) diff --git a/apps/builder/playwright/tests/workspaces.spec.ts b/apps/builder/playwright/tests/workspaces.spec.ts index 3df72294d..e73c3e72c 100644 --- a/apps/builder/playwright/tests/workspaces.spec.ts +++ b/apps/builder/playwright/tests/workspaces.spec.ts @@ -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"') diff --git a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx index 74f15c9eb..494767a21 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx @@ -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 diff --git a/packages/bot-engine/src/components/ConversationContainer.tsx b/packages/bot-engine/src/components/ConversationContainer.tsx index e58570026..6f61df848 100644 --- a/packages/bot-engine/src/components/ConversationContainer.tsx +++ b/packages/bot-engine/src/components/ConversationContainer.tsx @@ -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(null) const scrollableContainer = useRef(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 }, []) diff --git a/packages/bot-engine/src/components/TypebotViewer.tsx b/packages/bot-engine/src/components/TypebotViewer.tsx index 7f7568e4f..0759e84b8 100644 --- a/packages/bot-engine/src/components/TypebotViewer.tsx +++ b/packages/bot-engine/src/components/TypebotViewer.tsx @@ -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 onNewLog?: (log: Omit) => 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} /> {typebot.settings.general.isBrandingEnabled && }