Skip to content

Commit

Permalink
✨ (buttons) Allow dynamic buttons from variable
Browse files Browse the repository at this point in the history
Closes #237
  • Loading branch information
baptisteArno committed Feb 23, 2023
1 parent 8462810 commit 2ff6991
Show file tree
Hide file tree
Showing 28 changed files with 290 additions and 116 deletions.
4 changes: 3 additions & 1 deletion apps/builder/src/components/SearchableDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useDebouncedCallback } from 'use-debounce'
import { env, isDefined } from 'utils'
import { VariablesButton } from '@/features/variables'
import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'

type Props = {
selectedItem?: string
Expand Down Expand Up @@ -58,6 +59,7 @@ export const SearchableDropdown = ({
const dropdownRef = useRef(null)
const itemsRef = useRef<(HTMLButtonElement | null)[]>([])
const inputRef = useRef<HTMLInputElement>(null)
const { ref: parentModalRef } = useParentModal()

useEffect(
() => () => {
Expand Down Expand Up @@ -195,7 +197,7 @@ export const SearchableDropdown = ({
)}
</HStack>
</PopoverAnchor>
<Portal>
<Portal containerRef={parentModalRef}>
<PopoverContent
maxH="35vh"
overflowY="scroll"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ test.describe.parallel('Buttons input block', () => {
await page.click('[data-testid="block2-icon"]')
await page.click('text=Multiple choice?')
await page.fill('#button', 'Go')
await page.getByPlaceholder('Select a variable').click()
await page.getByPlaceholder('Select a variable').nth(1).click()
await page.getByText('var1').click()
await expect(page.getByText('Collectsvar1')).toBeVisible()
await page.click('[data-testid="block2-icon"]')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { BlockIndices, ChoiceInputBlock, Variable } from 'models'
import React from 'react'
import { ItemNodesList } from '@/features/graph/components/Nodes/ItemNode'
import {
HStack,
Stack,
Tag,
Text,
useColorModeValue,
Wrap,
} from '@chakra-ui/react'
import { useTypebot } from '@/features/editor'

type Props = {
block: ChoiceInputBlock
indices: BlockIndices
}

export const ButtonsBlockNode = ({ block, indices }: Props) => {
const { typebot } = useTypebot()
const dynamicVariableName = typebot?.variables.find(
(variable) => variable.id === block.options.dynamicVariableId
)?.name

return (
<Stack w="full">
{block.options.variableId ? (
<CollectVariableLabel
variableId={block.options.variableId}
variables={typebot?.variables}
/>
) : null}
{block.options.dynamicVariableId ? (
<Wrap spacing={1}>
<Text>Display</Text>
<Tag bg="orange.400" color="white">
{dynamicVariableName}
</Tag>
<Text>buttons</Text>
</Wrap>
) : (
<ItemNodesList block={block} indices={indices} />
)}
</Stack>
)
}

const CollectVariableLabel = ({
variableId,
variables,
}: {
variableId: string
variables?: Variable[]
}) => {
const textColor = useColorModeValue('gray.600', 'gray.400')
const variableName = variables?.find(
(variable) => variable.id === variableId
)?.name

if (!variableName) return null
return (
<HStack fontStyle="italic" spacing={1}>
<Text fontSize="sm" color={textColor}>
Collects
</Text>
<Tag bg="orange.400" color="white" size="sm">
{variableName}
</Tag>
</HStack>
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Input } from '@/components/inputs'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { SwitchWithLabel } from '@/components/SwitchWithLabel'
import { VariableSearchInput } from '@/components/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
import { ChoiceInputOptions, Variable } from 'models'
import React from 'react'

Expand All @@ -20,6 +21,8 @@ export const ButtonsOptionsForm = ({
options && onOptionsChange({ ...options, buttonLabel })
const handleVariableChange = (variable?: Variable) =>
options && onOptionsChange({ ...options, variableId: variable?.id })
const handleDynamicVariableChange = (variable?: Variable) =>
options && onOptionsChange({ ...options, dynamicVariableId: variable?.id })

return (
<Stack spacing={4}>
Expand All @@ -40,6 +43,19 @@ export const ButtonsOptionsForm = ({
/>
</Stack>
)}
<FormControl>
<FormLabel>
Dynamic items from variable:{' '}
<MoreInfoTooltip>
If defined, buttons will be dynamically displayed based on what the
variable contains.
</MoreInfoTooltip>
</FormLabel>
<VariableSearchInput
initialVariableId={options?.dynamicVariableId}
onSelectVariable={handleDynamicVariableChange}
/>
</FormControl>
<Stack>
<FormLabel mb="0" htmlFor="variable">
Save answer in a variable:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './ButtonsItemNode'
export * from './ButtonsIcon'
export * from './ButtonsOptionsForm'
export * from './ButtonsBlockSettings'
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@ export const getDeepKeys = (obj: any): string[] => {
const subkeys = getDeepKeys(obj[key])
keys = keys.concat(
subkeys.map(function (subkey) {
return key + '.' + subkey
return key + parseKey(subkey)
})
)
} else if (Array.isArray(obj[key])) {
for (let i = 0; i < obj[key].length; i++) {
const subkeys = getDeepKeys(obj[key][i])
keys = keys.concat(
subkeys.map(function (subkey) {
return key + '[' + i + ']' + '.' + subkey
})
)
}
const subkeys = getDeepKeys(obj[key][0])
keys = keys.concat(
subkeys.map(function (subkey) {
return `${key}.map(item => item${parseKey(subkey)})`
})
)
} else {
keys.push(key)
}
}
return keys
}

const parseKey = (key: string) => {
if (key.includes(' ') && !key.includes('.map((item) => item')) {
return `['${key}']`
}
return `.${key}`
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ test.describe('Builder', () => {
await page.click('text=Save in variables')
await page.click('text=Add an entry >> nth=-1')
await page.click('input[placeholder="Select the data"]')
await page.click('text=data[0].name')
await page.click('text=data.map(item => item.name)')
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,7 @@ export const BlockNode = ({

useEffect(() => {
if (query.blockId?.toString() === block.id) setOpenedBlockId(block.id)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
}, [block.id, query, setOpenedBlockId])

useEffect(() => {
setIsConnecting(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,18 @@ import { GoogleSheetsNodeContent } from '@/features/blocks/integrations/googleSh
import { GoogleAnalyticsNodeContent } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeContent'
import { ZapierContent } from '@/features/blocks/integrations/zapier'
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
import { isInputBlock, isChoiceInput, blockHasItems } from 'utils'
import { isInputBlock, isChoiceInput } from 'utils'
import { MakeComContent } from '@/features/blocks/integrations/makeCom'
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
import { ScriptNodeContent } from '@/features/blocks/logic/script/components/ScriptNodeContent'
import { ButtonsBlockNode } from '@/features/blocks/inputs/buttons/components/ButtonsBlockNode'

type Props = {
block: Block | StartBlock
indices: BlockIndices
}
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
if (blockHasItems(block))
return <ItemNodesList block={block} indices={indices} />

if (
isInputBlock(block) &&
!isChoiceInput(block) &&
Expand Down Expand Up @@ -92,6 +90,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
case InputBlockType.URL: {
return <UrlNodeContent placeholder={block.options.labels.placeholder} />
}
case InputBlockType.CHOICE: {
return <ButtonsBlockNode block={block} indices={indices} />
}
case InputBlockType.PHONE: {
return <PhoneNodeContent placeholder={block.options.labels.placeholder} />
}
Expand Down Expand Up @@ -126,7 +127,8 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
}
case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkNode block={block} />

case LogicBlockType.CONDITION:
return <ItemNodesList block={block} indices={indices} />
case IntegrationBlockType.GOOGLE_SHEETS: {
return (
<GoogleSheetsNodeContent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {
Flex,
HStack,
Portal,
Stack,
Tag,
Text,
useColorModeValue,
useEventListener,
Expand All @@ -15,13 +13,7 @@ import {
useGraph,
} from '../../../providers'
import { useTypebot } from '@/features/editor'
import {
BlockIndices,
BlockWithItems,
LogicBlockType,
Item,
Variable,
} from 'models'
import { BlockIndices, BlockWithItems, LogicBlockType, Item } from 'models'
import React, { useEffect, useRef, useState } from 'react'
import { ItemNode } from './ItemNode'
import { SourceEndpoint } from '../../Endpoints'
Expand Down Expand Up @@ -137,17 +129,8 @@ export const ItemNodesList = ({
elem && (placeholderRefs.current[idx] = elem)
}

const collectedVariableId =
'options' in block && block.options && block.options.variableId

return (
<Stack flex={1} spacing={1} maxW="full" onClick={stopPropagating}>
{collectedVariableId && (
<CollectVariableLabel
variableId={collectedVariableId}
variables={typebot?.variables ?? []}
/>
)}
<PlaceholderNode
isVisible={showPlaceholders}
isExpanded={expandedPlaceholderIndex === 0}
Expand Down Expand Up @@ -221,28 +204,3 @@ const DefaultItemNode = ({ block }: { block: BlockWithItems }) => {
</Flex>
)
}

const CollectVariableLabel = ({
variableId,
variables,
}: {
variableId: string
variables: Variable[]
}) => {
const textColor = useColorModeValue('gray.600', 'gray.400')
const variableName = variables.find(
(variable) => variable.id === variableId
)?.name

if (!variableName) return null
return (
<HStack fontStyle="italic" spacing={1}>
<Text fontSize="sm" color={textColor}>
Collects
</Text>
<Tag bg="orange.400" color="white" size="sm">
{variableName}
</Tag>
</HStack>
)
}
12 changes: 11 additions & 1 deletion apps/docs/openapi/builder/_spec_.json
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,17 @@
"type": "string"
},
"value": {
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"required": [
Expand Down
Loading

4 comments on commit 2ff6991

@vercel
Copy link

@vercel vercel bot commented on 2ff6991 Feb 23, 2023

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:

docs – ./apps/docs

docs-git-main-typebot-io.vercel.app
docs-typebot-io.vercel.app
docs.typebot.io

@vercel
Copy link

@vercel vercel bot commented on 2ff6991 Feb 23, 2023

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on 2ff6991 Feb 23, 2023

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

an.nigerias.io
app.yvon.earth
ar.nigerias.io
bot.enreso.org
bot.rslabs.pro
bots.bridge.ai
chat.hayuri.id
chicken.cr8.ai
gollum.riku.ai
gsbulletin.com
panther.cr7.ai
panther.cr8.ai
penguin.cr8.ai
talk.gocare.io
test.bot.gives
ticketfute.com
unicorn.cr8.ai
apo.nigerias.io
apr.nigerias.io
aso.nigerias.io
bot.ageenda.com
bot.artiweb.app
bot.devitus.com
bot.jesopizz.it
bot.reeplai.com
bot.scayver.com
bot.tc-mail.com
chat.lalmon.com
chat.sureb4.com
eventhub.com.au
fitness.riku.ai
games.klujo.com
help.taxtree.io
sakuranembro.it
typebot.aloe.do
bot.contakit.com
bot.piccinato.co
bot.sv-energy.it
botc.ceox.com.br
clo.closeer.work
cockroach.cr8.ai
faqs.nigerias.io
feedback.ofx.one
form.syncwin.com
haymanevents.com
kw.wpwakanda.com
myrentalhost.com
stan.vselise.com
start.taxtree.io
typebot.aloe.bot
voicehelp.cr8.ai
zap.fundviser.in
app.chatforms.net
bot.hostnation.de
bot.maitempah.com
bot.phuonghub.com
bot.reviewzer.com
bot.rihabilita.it
cares.urlabout.me
chat.gaswadern.de
fmm.wpwakanda.com
gentleman-shop.fr
k1.kandabrand.com
lb.ticketfute.com
ov1.wpwakanda.com
ov2.wpwakanda.com
ov3.wpwakanda.com
support.triplo.ai
viewer.typebot.io
1988.bouclidom.com
andreimayer.com.br
bot.danyservice.it
bot.iconicbrows.it
bot.megafox.com.br
bot.neferlopez.com
bots.robomotion.io
cadu.uninta.edu.br
dicanatural.online
digitalhelp.com.au
goalsettingbot.com
pant.maxbot.com.br
positivobra.com.br
survey.digienge.io
this-is-a-test.com
zap.techadviser.in
bot.boston-voip.com
bot.cabinpromos.com
bot.digitalbled.com
bot.dsignagency.com
bot.eventhub.com.au
bot.jepierre.com.br
bot.ltmidias.com.br
bbutton.wpwakanda.com
bot.coachayongzul.com
bot.digitalpointer.id
bot.eikju.photography
bot.incusservices.com
bot.meuesocial.com.br
bot.mycompany.reviews
bot.outstandbrand.com

@vercel
Copy link

@vercel vercel bot commented on 2ff6991 Feb 23, 2023

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

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

Please sign in to comment.