2
0

fix(dashboard): 🩹 typebot buttons menu

This commit is contained in:
Baptiste Arnaud
2022-01-29 12:44:56 +01:00
parent 1c5bd06657
commit fc1d654772
12 changed files with 160 additions and 94 deletions

View File

@@ -10,7 +10,7 @@ import { Block } from 'models'
import { useGraph } from 'contexts/GraphContext' import { useGraph } from 'contexts/GraphContext'
import { useStepDnd } from 'contexts/StepDndContext' import { useStepDnd } from 'contexts/StepDndContext'
import { StepsList } from './StepsList' import { StepsList } from './StepsList'
import { filterTable, isDefined } from 'utils' import { isDefined } from 'utils'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu' import { ContextMenu } from 'components/shared/ContextMenu'
import { BlockNodeContextMenu } from './BlockNodeContextMenu' import { BlockNodeContextMenu } from './BlockNodeContextMenu'
@@ -114,12 +114,7 @@ export const BlockNode = ({ block }: Props) => {
/> />
<EditableInput minW="0" px="1" /> <EditableInput minW="0" px="1" />
</Editable> </Editable>
{typebot && ( {typebot && <StepsList blockId={block.id} stepIds={block.stepIds} />}
<StepsList
blockId={block.id}
steps={filterTable(block.stepIds, typebot?.steps)}
/>
)}
</Stack> </Stack>
)} )}
</ContextMenu> </ContextMenu>

View File

@@ -44,7 +44,7 @@ export const StepNode = ({
const { query } = useRouter() const { query } = useRouter()
const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } = const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } =
useGraph() useGraph()
const { moveStep } = useTypebot() const { detachStepFromBlock } = useTypebot()
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
const [mouseDownEvent, setMouseDownEvent] = const [mouseDownEvent, setMouseDownEvent] =
useState<{ absolute: Coordinates; relative: Coordinates }>() useState<{ absolute: Coordinates; relative: Coordinates }>()
@@ -116,8 +116,9 @@ export const StepNode = ({
onMouseDown && onMouseDown &&
(event.movementX > 0 || event.movementY > 0) (event.movementX > 0 || event.movementY > 0)
if (isMovingAndIsMouseDown && step.type !== 'start') { if (isMovingAndIsMouseDown && step.type !== 'start') {
console.log(step)
onMouseDown(mouseDownEvent, step) onMouseDown(mouseDownEvent, step)
moveStep(step.id) detachStepFromBlock(step.id)
setMouseDownEvent(undefined) setMouseDownEvent(undefined)
} }
const element = event.currentTarget as HTMLDivElement const element = event.currentTarget as HTMLDivElement

View File

@@ -1,4 +1,4 @@
import { Box, Flex, Image, Text } from '@chakra-ui/react' import { Box, Image, Text } from '@chakra-ui/react'
import { import {
Step, Step,
StartStep, StartStep,
@@ -7,9 +7,12 @@ import {
LogicStepType, LogicStepType,
IntegrationStepType, IntegrationStepType,
} from 'models' } from 'models'
import { isInputStep } from 'utils'
import { ChoiceItemsList } from '../ChoiceInputStepNode/ChoiceItemsList' import { ChoiceItemsList } from '../ChoiceInputStepNode/ChoiceItemsList'
import { ConditionNodeContent } from './ConditionNodeContent' import { ConditionNodeContent } from './ConditionNodeContent'
import { SetVariableNodeContent } from './SetVariableNodeContent' import { SetVariableNodeContent } from './SetVariableNodeContent'
import { StepNodeContentWithVariable } from './StepNodeContentWithVariable'
import { TextBubbleNodeContent } from './TextBubbleNodeContent'
import { VideoStepNodeContent } from './VideoStepNodeContent' import { VideoStepNodeContent } from './VideoStepNodeContent'
import { WebhookContent } from './WebhookContent' import { WebhookContent } from './WebhookContent'
@@ -18,21 +21,12 @@ type Props = {
isConnectable?: boolean isConnectable?: boolean
} }
export const StepNodeContent = ({ step }: Props) => { export const StepNodeContent = ({ step }: Props) => {
if (isInputStep(step) && step.options.variableId) {
return <StepNodeContentWithVariable step={step} />
}
switch (step.type) { switch (step.type) {
case BubbleStepType.TEXT: { case BubbleStepType.TEXT: {
return ( return <TextBubbleNodeContent step={step} />
<Flex
flexDir={'column'}
opacity={step.content.html === '' ? '0.5' : '1'}
className="slate-html-container"
dangerouslySetInnerHTML={{
__html:
step.content.html === ''
? `<p>Click to edit...</p>`
: step.content.html,
}}
/>
)
} }
case BubbleStepType.IMAGE: { case BubbleStepType.IMAGE: {
return !step.content?.url ? ( return !step.content?.url ? (

View File

@@ -0,0 +1,28 @@
import { InputStep } from 'models'
import { chakra, Text } from '@chakra-ui/react'
import React from 'react'
import { useTypebot } from 'contexts/TypebotContext'
type Props = {
step: InputStep
}
export const StepNodeContentWithVariable = ({ step }: Props) => {
const { typebot } = useTypebot()
const variableName =
typebot?.variables.byId[step.options.variableId as string].name
return (
<Text>
Collect{' '}
<chakra.span
bgColor="orange.400"
color="white"
rounded="md"
py="0.5"
px="1"
>
{variableName}
</chakra.span>
</Text>
)
}

View File

@@ -0,0 +1,26 @@
import { Flex } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'
import { TextBubbleStep } from 'models'
import React from 'react'
import { parseVariableHighlight } from 'services/utils'
type Props = {
step: TextBubbleStep
}
export const TextBubbleNodeContent = ({ step }: Props) => {
const { typebot } = useTypebot()
return (
<Flex
flexDir={'column'}
opacity={step.content.html === '' ? '0.5' : '1'}
className="slate-html-container"
dangerouslySetInnerHTML={{
__html:
step.content.html === ''
? `<p>Click to edit...</p>`
: parseVariableHighlight(step.content.html, typebot),
}}
/>
)
}

View File

@@ -1,5 +1,5 @@
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react' import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
import { DraggableStep, Step, Table } from 'models' import { DraggableStep } from 'models'
import { useStepDnd } from 'contexts/StepDndContext' import { useStepDnd } from 'contexts/StepDndContext'
import { Coordinates } from 'contexts/GraphContext' import { Coordinates } from 'contexts/GraphContext'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
@@ -8,10 +8,10 @@ import { useTypebot } from 'contexts/TypebotContext'
export const StepsList = ({ export const StepsList = ({
blockId, blockId,
steps, stepIds,
}: { }: {
blockId: string blockId: string
steps: Table<Step> stepIds: string[]
}) => { }) => {
const { const {
draggedStep, draggedStep,
@@ -21,7 +21,7 @@ export const StepsList = ({
setDraggedStepType, setDraggedStepType,
setMouseOverBlockId, setMouseOverBlockId,
} = useStepDnd() } = useStepDnd()
const { createStep } = useTypebot() const { typebot, createStep } = useTypebot()
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState< const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
number | undefined number | undefined
>() >()
@@ -59,14 +59,14 @@ export const StepsList = ({
e.stopPropagation() e.stopPropagation()
setMouseOverBlockId(undefined) setMouseOverBlockId(undefined)
setExpandedPlaceholderIndex(undefined) setExpandedPlaceholderIndex(undefined)
if (draggedStepType) { if (!draggedStep && !draggedStepType) return
createStep(blockId, draggedStepType, expandedPlaceholderIndex) createStep(
setDraggedStepType(undefined) blockId,
} draggedStep || draggedStepType,
if (draggedStep) { expandedPlaceholderIndex
createStep(blockId, draggedStep, expandedPlaceholderIndex) )
setDraggedStep(undefined) setDraggedStep(undefined)
} setDraggedStepType(undefined)
} }
const handleStepMouseDown = ( const handleStepMouseDown = (
@@ -107,31 +107,32 @@ export const StepsList = ({
rounded="lg" rounded="lg"
transition={showSortPlaceholders ? 'height 200ms' : 'none'} transition={showSortPlaceholders ? 'height 200ms' : 'none'}
/> />
{steps.allIds.map((stepId, idx) => ( {typebot &&
<Stack key={stepId} spacing={1}> stepIds.map((stepId, idx) => (
<StepNode <Stack key={stepId} spacing={1}>
key={stepId} <StepNode
step={steps.byId[stepId]} key={stepId}
isConnectable={steps.allIds.length - 1 === idx} step={typebot?.steps.byId[stepId]}
onMouseMoveTopOfElement={() => handleMouseOnTopOfStep(idx)} isConnectable={stepIds.length - 1 === idx}
onMouseMoveBottomOfElement={() => { onMouseMoveTopOfElement={() => handleMouseOnTopOfStep(idx)}
handleMouseOnBottomOfStep(idx) onMouseMoveBottomOfElement={() => {
}} handleMouseOnBottomOfStep(idx)
onMouseDown={handleStepMouseDown} }}
/> onMouseDown={handleStepMouseDown}
<Flex />
h={ <Flex
showSortPlaceholders && expandedPlaceholderIndex === idx + 1 h={
? '50px' showSortPlaceholders && expandedPlaceholderIndex === idx + 1
: '2px' ? '50px'
} : '2px'
bgColor={'gray.300'} }
visibility={showSortPlaceholders ? 'visible' : 'hidden'} bgColor={'gray.300'}
rounded="lg" visibility={showSortPlaceholders ? 'visible' : 'hidden'}
transition={showSortPlaceholders ? 'height 200ms' : 'none'} rounded="lg"
/> transition={showSortPlaceholders ? 'height 200ms' : 'none'}
</Stack> />
))} </Stack>
))}
{draggedStep && draggedStep.blockId === blockId && ( {draggedStep && draggedStep.blockId === blockId && (
<Portal> <Portal>
<StepNodeOverlay <StepNodeOverlay

View File

@@ -14,7 +14,7 @@ import {
import { FolderPlusIcon } from 'assets/icons' import { FolderPlusIcon } from 'assets/icons'
import { useTypebotDnd } from 'contexts/TypebotDndContext' import { useTypebotDnd } from 'contexts/TypebotDndContext'
import { Typebot } from 'models' import { Typebot } from 'models'
import React, { useEffect, useState } from 'react' import React, { useState } from 'react'
import { createFolder, useFolders } from 'services/folders' import { createFolder, useFolders } from 'services/folders'
import { patchTypebot, useTypebots } from 'services/typebots' import { patchTypebot, useTypebots } from 'services/typebots'
import { BackButton } from './FolderContent/BackButton' import { BackButton } from './FolderContent/BackButton'

View File

@@ -16,8 +16,7 @@ export const MoreButton = ({ children, ...props }: Props) => {
<MenuButton <MenuButton
as={IconButton} as={IconButton}
icon={<MoreVerticalIcon />} icon={<MoreVerticalIcon />}
onMouseUp={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
colorScheme="gray" colorScheme="gray"
variant="outline" variant="outline"
size="sm" size="sm"

View File

@@ -18,6 +18,7 @@ import { GlobeIcon, GripIcon, ToolIcon } from 'assets/icons'
import { deleteTypebot, duplicateTypebot } from 'services/typebots' import { deleteTypebot, duplicateTypebot } from 'services/typebots'
import { Typebot } from 'models' import { Typebot } from 'models'
import { useTypebotDnd } from 'contexts/TypebotDndContext' import { useTypebotDnd } from 'contexts/TypebotDndContext'
import { useDebounce } from 'use-debounce'
type ChatbotCardProps = { type ChatbotCardProps = {
typebot: Typebot typebot: Typebot
@@ -32,6 +33,7 @@ export const TypebotButton = ({
}: ChatbotCardProps) => { }: ChatbotCardProps) => {
const router = useRouter() const router = useRouter()
const { draggedTypebot } = useTypebotDnd() const { draggedTypebot } = useTypebotDnd()
const [draggedTypebotDebounced] = useDebounce(draggedTypebot, 200)
const { const {
isOpen: isDeleteOpen, isOpen: isDeleteOpen,
onOpen: onDeleteOpen, onOpen: onDeleteOpen,
@@ -44,7 +46,7 @@ export const TypebotButton = ({
}) })
const handleTypebotClick = () => { const handleTypebotClick = () => {
if (draggedTypebot) return if (draggedTypebotDebounced) return
router.push( router.push(
isMobile isMobile
? `/typebots/${typebot.id}/results/responses` ? `/typebots/${typebot.id}/results/responses`
@@ -72,10 +74,15 @@ export const TypebotButton = ({
if (createdTypebot) router.push(`/typebots/${createdTypebot?.id}`) if (createdTypebot) router.push(`/typebots/${createdTypebot?.id}`)
} }
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation()
onDeleteOpen()
}
return ( return (
<Button <Button
as={WrapItem} as={WrapItem}
onMouseUp={handleTypebotClick} onClick={handleTypebotClick}
display="flex" display="flex"
flexDir="column" flexDir="column"
variant="outline" variant="outline"
@@ -108,13 +115,7 @@ export const TypebotButton = ({
aria-label={`Show ${typebot.name} menu`} aria-label={`Show ${typebot.name} menu`}
> >
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem> <MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
<MenuItem <MenuItem color="red" onClick={handleDeleteClick}>
color="red"
onClick={(e) => {
e.stopPropagation()
onDeleteOpen()
}}
>
Delete Delete
</MenuItem> </MenuItem>
</MoreButton> </MoreButton>

View File

@@ -32,7 +32,7 @@ import { edgesAction, EdgesActions } from './actions/edges'
import { webhooksAction, WebhooksAction } from './actions/webhooks' import { webhooksAction, WebhooksAction } from './actions/webhooks'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
const autoSaveTimeout = 10000 const autoSaveTimeout = 40000
type UpdateTypebotPayload = Partial<{ type UpdateTypebotPayload = Partial<{
theme: Theme theme: Theme

View File

@@ -17,23 +17,24 @@ import { deleteWebhookDraft } from './webhooks'
export type StepsActions = { export type StepsActions = {
createStep: ( createStep: (
blockId: string, blockId: string,
step: DraggableStep | DraggableStepType, step?: DraggableStep | DraggableStepType,
index?: number index?: number
) => void ) => void
updateStep: ( updateStep: (
stepId: string, stepId: string,
updates: Partial<Omit<Step, 'id' | 'type'>> updates: Partial<Omit<Step, 'id' | 'type'>>
) => void ) => void
moveStep: (stepId: string) => void detachStepFromBlock: (stepId: string) => void
deleteStep: (stepId: string) => void deleteStep: (stepId: string) => void
} }
export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({ export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
createStep: ( createStep: (
blockId: string, blockId: string,
step: DraggableStep | DraggableStepType, step?: DraggableStep | DraggableStepType,
index?: number index?: number
) => { ) => {
if (!step) return
setTypebot((typebot) => { setTypebot((typebot) => {
createStepDraft(typebot, step, blockId, index) createStepDraft(typebot, step, blockId, index)
removeEmptyBlocks(typebot) removeEmptyBlocks(typebot)
@@ -43,7 +44,7 @@ export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
setTypebot((typebot) => { setTypebot((typebot) => {
typebot.steps.byId[stepId] = { ...typebot.steps.byId[stepId], ...updates } typebot.steps.byId[stepId] = { ...typebot.steps.byId[stepId], ...updates }
}), }),
moveStep: (stepId: string) => { detachStepFromBlock: (stepId: string) => {
setTypebot((typebot) => { setTypebot((typebot) => {
removeStepIdFromBlock(typebot, stepId) removeStepIdFromBlock(typebot, stepId)
}) })
@@ -54,23 +55,13 @@ export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step) if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step)
if (isWebhookStep(step)) if (isWebhookStep(step))
deleteWebhookDraft(step.options?.webhookId)(typebot) deleteWebhookDraft(step.options?.webhookId)(typebot)
deleteAssociatedEdges(typebot, stepId) deleteEdgeDraft(typebot, step.edgeId)
removeStepIdFromBlock(typebot, stepId) removeStepIdFromBlock(typebot, stepId)
deleteStepDraft(typebot, stepId) deleteStepDraft(typebot, stepId)
}) })
}, },
}) })
const deleteAssociatedEdges = (
typebot: WritableDraft<Typebot>,
stepId: string
) => {
typebot.edges.allIds.forEach((edgeId) => {
if (typebot.edges.byId[edgeId].from.stepId === stepId)
deleteEdgeDraft(typebot, edgeId)
})
}
const removeStepIdFromBlock = ( const removeStepIdFromBlock = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
stepId: string stepId: string
@@ -93,18 +84,32 @@ export const createStepDraft = (
step: DraggableStep | DraggableStepType, step: DraggableStep | DraggableStepType,
blockId: string, blockId: string,
index?: number index?: number
) =>
typeof step === 'string'
? createNewStep(typebot, step, blockId, index)
: moveStepToBlock(typebot, step, blockId, index)
const createNewStep = (
typebot: WritableDraft<Typebot>,
type: DraggableStepType,
blockId: string,
index?: number
) => { ) => {
const newStep = const newStep = parseNewStep(type, blockId)
typeof step === 'string'
? parseNewStep(step, blockId)
: { ...step, blockId }
typebot.steps.byId[newStep.id] = newStep typebot.steps.byId[newStep.id] = newStep
if (isChoiceInput(newStep) && newStep.options.itemIds.length === 0) if (isChoiceInput(newStep))
createChoiceItemDraft(typebot, { stepId: newStep.id }) createChoiceItemDraft(typebot, { stepId: newStep.id })
typebot.steps.allIds.push(newStep.id) typebot.steps.allIds.push(newStep.id)
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, newStep.id) typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, newStep.id)
} }
const moveStepToBlock = (
typebot: WritableDraft<Typebot>,
step: DraggableStep,
blockId: string,
index?: number
) => typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, step.id)
const deleteChoiceItemsInsideStep = ( const deleteChoiceItemsInsideStep = (
typebot: WritableDraft<Typebot>, typebot: WritableDraft<Typebot>,
step: ChoiceInputStep step: ChoiceInputStep

View File

@@ -1,6 +1,6 @@
import imageCompression from 'browser-image-compression' import imageCompression from 'browser-image-compression'
import { Parser } from 'htmlparser2' import { Parser } from 'htmlparser2'
import { Step } from 'models' import { Step, Typebot } from 'models'
export const fetcher = async (input: RequestInfo, init?: RequestInit) => { export const fetcher = async (input: RequestInfo, init?: RequestInit) => {
const res = await fetch(input, init) const res = await fetch(input, init)
@@ -99,3 +99,19 @@ export const removeUndefinedFields = <T>(obj: T): T =>
) )
export const stepHasOptions = (step: Step) => 'options' in step export const stepHasOptions = (step: Step) => 'options' in step
export const parseVariableHighlight = (content: string, typebot?: Typebot) => {
if (!typebot) return content
const varNames = typebot.variables.allIds.map(
(varId) => typebot.variables.byId[varId].name
)
return content.replace(/\{\{(.*?)\}\}/g, (fullMatch, foundVar) => {
if (varNames.some((val) => foundVar.includes(val))) {
return `<span style="background-color:#ff8b1a; color:#ffffff; padding: 0.125rem 0.25rem; border-radius: 0.35rem">${fullMatch.replace(
/{{|}}/g,
''
)}</span>`
}
return fullMatch
})
}