Skip to content

Commit

Permalink
feat(inputs): ✨ Add buttons input
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Jan 12, 2022
1 parent b20bcb1 commit c02c61c
Show file tree
Hide file tree
Showing 47 changed files with 1,108 additions and 242 deletions.
7 changes: 7 additions & 0 deletions apps/builder/assets/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,10 @@ export const PhoneIcon = (props: IconProps) => (
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
</Icon>
)

export const CheckSquareIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<polyline points="9 11 12 14 22 4"></polyline>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</Icon>
)
8 changes: 4 additions & 4 deletions apps/builder/components/analytics/graph/Edges/Edge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ export const Edge = ({ stepId }: Props) => {

const path = useMemo(() => {
if (!sourceBlock || !targetBlock) return ``
const anchorsPosition = getAnchorsPosition(
const anchorsPosition = getAnchorsPosition({
sourceBlock,
targetBlock,
sourceBlock.stepIds.indexOf(stepId),
targetStepIndex
)
sourceStepIndex: sourceBlock.stepIds.indexOf(stepId),
sourceChoiceItemIndex: targetStepIndex,
})
return computeFlowChartConnectorPath(anchorsPosition)
}, [sourceBlock, stepId, targetBlock, targetStepIndex])

Expand Down
25 changes: 15 additions & 10 deletions apps/builder/components/board/StepTypesList/StepIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IconProps } from '@chakra-ui/react'
import {
CalendarIcon,
ChatIcon,
CheckSquareIcon,
EmailIcon,
FlagIcon,
GlobeIcon,
Expand All @@ -11,33 +13,36 @@ import {
import { BubbleStepType, InputStepType, StepType } from 'models'
import React from 'react'

type StepIconProps = { type: StepType }
type StepIconProps = { type: StepType } & IconProps

export const StepIcon = ({ type }: StepIconProps) => {
export const StepIcon = ({ type, ...props }: StepIconProps) => {
switch (type) {
case BubbleStepType.TEXT: {
return <ChatIcon />
return <ChatIcon {...props} />
}
case InputStepType.TEXT: {
return <TextIcon />
return <TextIcon {...props} />
}
case InputStepType.NUMBER: {
return <NumberIcon />
return <NumberIcon {...props} />
}
case InputStepType.EMAIL: {
return <EmailIcon />
return <EmailIcon {...props} />
}
case InputStepType.URL: {
return <GlobeIcon />
return <GlobeIcon {...props} />
}
case InputStepType.DATE: {
return <CalendarIcon />
return <CalendarIcon {...props} />
}
case InputStepType.PHONE: {
return <PhoneIcon />
return <PhoneIcon {...props} />
}
case InputStepType.CHOICE: {
return <CheckSquareIcon {...props} />
}
case 'start': {
return <FlagIcon />
return <FlagIcon {...props} />
}
default: {
return <></>
Expand Down
3 changes: 3 additions & 0 deletions apps/builder/components/board/StepTypesList/StepTypeLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export const StepTypeLabel = ({ type }: Props) => {
case InputStepType.PHONE: {
return <Text>Phone</Text>
}
case InputStepType.CHOICE: {
return <Text>Button</Text>
}
default: {
return <></>
}
Expand Down
25 changes: 5 additions & 20 deletions apps/builder/components/board/graph/BlockNode/BlockNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ type Props = {

export const BlockNode = ({ block }: Props) => {
const { connectingIds, setConnectingIds, previewingIds } = useGraph()
const { typebot, updateBlock, createStep } = useTypebot()
const { draggedStep, draggedStepType, setDraggedStepType, setDraggedStep } =
useDnd()
const { typebot, updateBlock } = useTypebot()
const { setMouseOverBlockId } = useDnd()
const { draggedStep, draggedStepType } = useDnd()
const [isMouseDown, setIsMouseDown] = useState(false)
const [titleValue, setTitleValue] = useState(block.title)
const [showSortPlaceholders, setShowSortPlaceholders] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const isPreviewing = useMemo(
() =>
Expand Down Expand Up @@ -66,28 +65,16 @@ export const BlockNode = ({ block }: Props) => {
useEventListener('mousemove', handleMouseMove)

const handleMouseEnter = () => {
if (draggedStepType || draggedStep) setShowSortPlaceholders(true)
if (draggedStepType || draggedStep) setMouseOverBlockId(block.id)
if (connectingIds)
setConnectingIds({ ...connectingIds, target: { blockId: block.id } })
}

const handleMouseLeave = () => {
setShowSortPlaceholders(false)
setMouseOverBlockId(undefined)
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
}

const handleStepDrop = (index: number) => {
setShowSortPlaceholders(false)
if (draggedStepType) {
createStep(block.id, draggedStepType, index)
setDraggedStepType(undefined)
}
if (draggedStep) {
createStep(block.id, draggedStep, index)
setDraggedStep(undefined)
}
}

return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <BlockNodeContextMenu blockId={block.id} />}
Expand Down Expand Up @@ -125,8 +112,6 @@ export const BlockNode = ({ block }: Props) => {
<StepsList
blockId={block.id}
steps={filterTable(block.stepIds, typebot?.steps)}
showSortPlaceholders={showSortPlaceholders}
onMouseUp={handleStepDrop}
/>
)}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {
EditablePreview,
EditableInput,
Editable,
useEventListener,
Flex,
Fade,
IconButton,
} from '@chakra-ui/react'
import { PlusIcon } from 'assets/icons'
import { ContextMenu } from 'components/shared/ContextMenu'
import { Coordinates } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext'
import { ChoiceInputStep, ChoiceItem } from 'models'
import React, { useState } from 'react'
import { isDefined, isSingleChoiceInput } from 'utils'
import { SourceEndpoint } from '../SourceEndpoint'
import { ChoiceItemNodeContextMenu } from './ChoiceItemNodeContextMenu'

type ChoiceItemNodeProps = {
item: ChoiceItem
onMouseMoveBottomOfElement?: () => void
onMouseMoveTopOfElement?: () => void
onMouseDown?: (
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
item: ChoiceItem
) => void
}

export const ChoiceItemNode = ({
item,
onMouseDown,
onMouseMoveBottomOfElement,
onMouseMoveTopOfElement,
}: ChoiceItemNodeProps) => {
const { deleteChoiceItem, updateChoiceItem, typebot, createChoiceItem } =
useTypebot()
const [mouseDownEvent, setMouseDownEvent] =
useState<{ absolute: Coordinates; relative: Coordinates }>()
const [isMouseOver, setIsMouseOver] = useState(false)

const handleMouseDown = (e: React.MouseEvent) => {
if (!onMouseDown) return
e.stopPropagation()
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const relativeX = e.clientX - rect.left
const relativeY = e.clientY - rect.top
setMouseDownEvent({
absolute: { x: e.clientX + relativeX, y: e.clientY + relativeY },
relative: { x: relativeX, y: relativeY },
})
}

const handleGlobalMouseUp = () => {
setMouseDownEvent(undefined)
}
useEventListener('mouseup', handleGlobalMouseUp)

const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!onMouseMoveBottomOfElement || !onMouseMoveTopOfElement) return
const isMovingAndIsMouseDown =
mouseDownEvent &&
onMouseDown &&
(event.movementX > 0 || event.movementY > 0)
if (isMovingAndIsMouseDown) {
onMouseDown(mouseDownEvent, item)
deleteChoiceItem(item.id)
setMouseDownEvent(undefined)
}
const element = event.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const y = event.clientY - rect.top
if (y > rect.height / 2) onMouseMoveBottomOfElement()
else onMouseMoveTopOfElement()
}

const handleInputSubmit = (content: string) =>
updateChoiceItem(item.id, { content: content === '' ? undefined : content })

const handlePlusClick = () => {
const nextIndex =
(
typebot?.steps.byId[item.stepId] as ChoiceInputStep
).options.itemIds.indexOf(item.id) + 1
createChoiceItem({ stepId: item.stepId }, nextIndex)
}

const handleMouseEnter = () => setIsMouseOver(true)
const handleMouseLeave = () => setIsMouseOver(false)
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <ChoiceItemNodeContextMenu itemId={item.id} />}
>
{(ref, isOpened) => (
<Flex
align="center"
pos="relative"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
justifyContent="center"
>
<Editable
ref={ref}
px="4"
py="2"
rounded="md"
bgColor="green.200"
borderWidth="2px"
borderColor={isOpened ? 'blue.400' : 'gray.400'}
defaultValue={item.content ?? 'Click to edit'}
flex="1"
startWithEditView={!isDefined(item.content)}
onSubmit={handleInputSubmit}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
>
<EditablePreview />
<EditableInput />
</Editable>
{typebot && isSingleChoiceInput(typebot.steps.byId[item.stepId]) && (
<SourceEndpoint
source={{
blockId: typebot.steps.byId[item.stepId].blockId,
stepId: item.stepId,
choiceItemId: item.id,
}}
pos="absolute"
right="15px"
/>
)}
<Fade
in={isMouseOver}
style={{ position: 'absolute', bottom: '-15px', zIndex: 3 }}
unmountOnExit
>
<IconButton
aria-label="Add item"
icon={<PlusIcon />}
size="xs"
onClick={handlePlusClick}
/>
</Fade>
</Flex>
)}
</ContextMenu>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MenuList, MenuItem } from '@chakra-ui/react'
import { TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'

export const ChoiceItemNodeContextMenu = ({ itemId }: { itemId: string }) => {
const { deleteChoiceItem } = useTypebot()

const handleDeleteClick = () => deleteChoiceItem(itemId)

return (
<MenuList>
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
Delete
</MenuItem>
</MenuList>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Flex, FlexProps } from '@chakra-ui/react'
import { ChoiceItem } from 'models'
import React from 'react'

type ChoiceItemNodeOverlayProps = {
item: ChoiceItem
} & FlexProps

export const ChoiceItemNodeOverlay = ({
item,
...props
}: ChoiceItemNodeOverlayProps) => {
return (
<Flex
px="4"
py="2"
rounded="md"
bgColor="green.200"
borderWidth="2px"
borderColor={'gray.400'}
w="212px"
pointerEvents="none"
{...props}
>
{item.content ?? 'Click to edit'}
</Flex>
)
}
Loading

2 comments on commit c02c61c

@vercel
Copy link

@vercel vercel bot commented on c02c61c Jan 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

typebot-io.vercel.app
viewer-v2-typebot-io.vercel.app
viewer-v2-git-main-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on c02c61c Jan 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-git-main-typebot-io.vercel.app
next.typebot.io
builder-v2-typebot-io.vercel.app

Please sign in to comment.