2
0

🐛 (editor) Fix variable dropdown overflow

Closes #209
This commit is contained in:
Baptiste Arnaud
2023-01-04 15:35:11 +01:00
parent e1af6af9c8
commit c1a32ce26b
10 changed files with 125 additions and 94 deletions

View File

@ -1,6 +1,5 @@
import { import {
useDisclosure, useDisclosure,
useOutsideClick,
Flex, Flex,
Popover, Popover,
Input, Input,
@ -16,6 +15,7 @@ import { useState, useRef, useEffect, ChangeEvent, ReactNode } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import { env, isDefined } from 'utils' import { env, isDefined } from 'utils'
import { VariablesButton } from '@/features/variables' import { VariablesButton } from '@/features/variables'
import { useOutsideClick } from '@/hooks/useOutsideClick'
type Props = { type Props = {
selectedItem?: string selectedItem?: string

View File

@ -10,7 +10,7 @@ import {
HStack, HStack,
useColorModeValue, useColorModeValue,
PopoverAnchor, PopoverAnchor,
useOutsideClick, Portal,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons' import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
@ -19,6 +19,7 @@ import { Variable } from 'models'
import React, { useState, useRef, ChangeEvent, useEffect } from 'react' import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import { byId, env, isDefined, isNotDefined } from 'utils' import { byId, env, isDefined, isNotDefined } from 'utils'
import { useOutsideClick } from '@/hooks/useOutsideClick'
type Props = { type Props = {
initialVariableId?: string initialVariableId?: string
@ -188,75 +189,78 @@ export const VariableSearchInput = ({
{...inputProps} {...inputProps}
/> />
</PopoverAnchor> </PopoverAnchor>
<PopoverContent <Portal>
maxH="35vh" <PopoverContent
overflowY="scroll" maxH="35vh"
role="menu" overflowY="scroll"
w="inherit" role="menu"
shadow="lg" w="inherit"
> shadow="lg"
{isCreateVariableButtonDisplayed && ( >
<Button {isCreateVariableButtonDisplayed && (
ref={createVariableItemRef} <Button
role="menuitem" ref={createVariableItemRef}
minH="40px" role="menuitem"
onClick={handleCreateNewVariableClick} minH="40px"
fontSize="16px" onClick={handleCreateNewVariableClick}
fontWeight="normal" fontSize="16px"
rounded="none" fontWeight="normal"
colorScheme="gray" rounded="none"
variant="ghost" colorScheme="gray"
justifyContent="flex-start" variant="ghost"
leftIcon={<PlusIcon />} justifyContent="flex-start"
bgColor={keyboardFocusIndex === 0 ? bg : 'transparent'} leftIcon={<PlusIcon />}
> bgColor={keyboardFocusIndex === 0 ? bg : 'transparent'}
Create &quot;{inputValue}&quot; >
</Button> Create &quot;{inputValue}&quot;
)} </Button>
{filteredItems.length > 0 && ( )}
<> {filteredItems.length > 0 && (
{filteredItems.map((item, idx) => { <>
const indexInList = isCreateVariableButtonDisplayed {filteredItems.map((item, idx) => {
? idx + 1 const indexInList = isCreateVariableButtonDisplayed
: idx ? idx + 1
return ( : idx
<Button return (
ref={(el) => (itemsRef.current[idx] = el)} <Button
role="menuitem" ref={(el) => (itemsRef.current[idx] = el)}
minH="40px" role="menuitem"
key={idx} minH="40px"
onClick={handleVariableNameClick(item)} key={idx}
fontSize="16px" onMouseDown={(e) => e.stopPropagation()}
fontWeight="normal" onClick={handleVariableNameClick(item)}
rounded="none" fontSize="16px"
colorScheme="gray" fontWeight="normal"
variant="ghost" rounded="none"
justifyContent="space-between" colorScheme="gray"
bgColor={ variant="ghost"
keyboardFocusIndex === indexInList ? bg : 'transparent' justifyContent="space-between"
} bgColor={
> keyboardFocusIndex === indexInList ? bg : 'transparent'
{item.name} }
<HStack> >
<IconButton {item.name}
icon={<EditIcon />} <HStack>
aria-label="Rename variable" <IconButton
size="xs" icon={<EditIcon />}
onClick={handleRenameVariableClick(item)} aria-label="Rename variable"
/> size="xs"
<IconButton onClick={handleRenameVariableClick(item)}
icon={<TrashIcon />} />
aria-label="Remove variable" <IconButton
size="xs" icon={<TrashIcon />}
onClick={handleDeleteVariableClick(item)} aria-label="Remove variable"
/> size="xs"
</HStack> onClick={handleDeleteVariableClick(item)}
</Button> />
) </HStack>
})} </Button>
</> )
)} })}
</PopoverContent> </>
)}
</PopoverContent>
</Portal>
</Popover> </Popover>
</Flex> </Flex>
) )

View File

@ -3,7 +3,6 @@ import {
Stack, Stack,
useColorModeValue, useColorModeValue,
useEventListener, useEventListener,
useOutsideClick,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { import {
@ -22,6 +21,7 @@ import { serializeHtml } from '@udecode/plate-serializer-html'
import { parseHtmlStringToPlainText } from '../../utils' import { parseHtmlStringToPlainText } from '../../utils'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/VariableSearchInput'
import { colors } from '@/lib/theme' import { colors } from '@/lib/theme'
import { useOutsideClick } from '@/hooks/useOutsideClick'
type TextBubbleEditorContentProps = { type TextBubbleEditorContentProps = {
id: string id: string

View File

@ -193,7 +193,7 @@ export const WebhookSettings = ({
Query params Query params
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6"> <AccordionPanel py={4} as={Stack} spacing="6">
<TableList<KeyValue> <TableList<KeyValue>
initialItems={localWebhook.queryParams} initialItems={localWebhook.queryParams}
onItemsChange={handleQueryParamsChange} onItemsChange={handleQueryParamsChange}
@ -208,7 +208,7 @@ export const WebhookSettings = ({
Headers Headers
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6"> <AccordionPanel py={4} as={Stack} spacing="6">
<TableList<KeyValue> <TableList<KeyValue>
initialItems={localWebhook.headers} initialItems={localWebhook.headers}
onItemsChange={handleHeadersChange} onItemsChange={handleHeadersChange}
@ -223,7 +223,7 @@ export const WebhookSettings = ({
Body Body
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6"> <AccordionPanel py={4} as={Stack} spacing="6">
<SwitchWithLabel <SwitchWithLabel
label="Custom body" label="Custom body"
initialValue={options.isCustomBody ?? true} initialValue={options.isCustomBody ?? true}
@ -244,7 +244,7 @@ export const WebhookSettings = ({
Variable values for test Variable values for test
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6"> <AccordionPanel py={4} as={Stack} spacing="6">
<TableList<VariableForTest> <TableList<VariableForTest>
initialItems={ initialItems={
options?.variablesForTest ?? { byId: {}, allIds: [] } options?.variablesForTest ?? { byId: {}, allIds: [] }
@ -279,7 +279,7 @@ export const WebhookSettings = ({
Save in variables Save in variables
<AccordionIcon /> <AccordionIcon />
</AccordionButton> </AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6"> <AccordionPanel py={4} as={Stack} spacing="6">
<TableList<ResponseVariableMapping> <TableList<ResponseVariableMapping>
initialItems={options.responseVariableMapping} initialItems={options.responseVariableMapping}
onItemsChange={handleResponseMappingChange} onItemsChange={handleResponseMappingChange}

View File

@ -120,10 +120,6 @@ const NonMemoizedDraggableGroupNode = ({
const handleTitleSubmit = (title: string) => const handleTitleSubmit = (title: string) =>
title.length > 0 ? updateGroup(groupIndex, { title }) : undefined title.length > 0 ? updateGroup(groupIndex, { title }) : undefined
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
}
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (isReadOnly) return if (isReadOnly) return
if (mouseOverGroup?.id !== group.id && !isStartGroup) if (mouseOverGroup?.id !== group.id && !isStartGroup)
@ -200,7 +196,6 @@ const NonMemoizedDraggableGroupNode = ({
currentCoordinates?.y ?? 0 currentCoordinates?.y ?? 0
}px)`, }px)`,
}} }}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
cursor={isMouseDown ? 'grabbing' : 'pointer'} cursor={isMouseDown ? 'grabbing' : 'pointer'}

View File

@ -342,7 +342,7 @@ export const getEndpointTopOffset = ({
if (!endpointId) return if (!endpointId) return
const endpointRef = endpoints[endpointId]?.ref const endpointRef = endpoints[endpointId]?.ref
if (!endpointRef?.current) return if (!endpointRef?.current) return
const endpointHeight = 28 * graphScale const endpointHeight = 34 * graphScale
return ( return (
(endpointRef.current.getBoundingClientRect().y + (endpointRef.current.getBoundingClientRect().y +
endpointHeight / 2 - endpointHeight / 2 -

View File

@ -13,6 +13,7 @@ import {
InputRightElement, InputRightElement,
ModalFooter, ModalFooter,
Link, Link,
useColorModeValue,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ExternalLinkIcon } from '@/components/icons' import { ExternalLinkIcon } from '@/components/icons'
import { env, getViewerUrl } from 'utils' import { env, getViewerUrl } from 'utils'
@ -44,7 +45,7 @@ export const WordpressModal = ({
<Link <Link
href="https://wordpress.org/plugins/typebot/" href="https://wordpress.org/plugins/typebot/"
isExternal isExternal
color="blue.500" color={useColorModeValue('blue.500', 'blue.300')}
> >
the official Typebot WordPress plugin the official Typebot WordPress plugin
<ExternalLinkIcon mx="2px" /> <ExternalLinkIcon mx="2px" />

View File

@ -68,8 +68,8 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
> >
<ModalOverlay /> <ModalOverlay />
<ModalContent h="85vh"> <ModalContent h="85vh">
<ModalBody as={HStack} p="0"> <ModalBody as={HStack} p="0" spacing="0">
<Stack w="full" h="full"> <Stack w="full" h="full" spacing="4">
<Heading pl="10" pt="4" fontSize="2xl"> <Heading pl="10" pt="4" fontSize="2xl">
{selectedTemplate.emoji}{' '} {selectedTemplate.emoji}{' '}
<chakra.span ml="2">{selectedTemplate.name}</chakra.span> <chakra.span ml="2">{selectedTemplate.name}</chakra.span>
@ -79,6 +79,7 @@ export const TemplatesModal = ({ isOpen, onClose, onTypebotChoose }: Props) => {
apiHost={getViewerUrl({ isBuilder: true })} apiHost={getViewerUrl({ isBuilder: true })}
typebot={parseTypebotToPublicTypebot(typebot)} typebot={parseTypebotToPublicTypebot(typebot)}
key={typebot.id} key={typebot.id}
style={{ borderRadius: '0.25rem' }}
/> />
)} )}
</Stack> </Stack>

View File

@ -0,0 +1,26 @@
import { useEventListener } from '@chakra-ui/react'
import { RefObject } from 'react'
type Handler = (event: MouseEvent) => void
type Props<T> = {
ref: RefObject<T>
handler: Handler
mouseEvent?: 'mousedown' | 'mouseup'
}
export const useOutsideClick = <T extends HTMLElement = HTMLElement>({
ref,
handler,
mouseEvent = 'mousedown',
}: Props<T>): void => {
const triggerHandlerIfOutside = (event: MouseEvent) => {
const el = ref?.current
if (!el || el.contains(event.target as Node)) {
return
}
handler(event)
}
useEventListener(mouseEvent, triggerHandlerIfOutside)
}

View File

@ -8,9 +8,9 @@ import {
Group, Group,
InputBlockType, InputBlockType,
PublicTypebot, PublicTypebot,
publicTypebotSchema,
Theme, Theme,
Typebot, Typebot,
typebotSchema,
} from 'models' } from 'models'
import { isDefined, isNotDefined } from 'utils' import { isDefined, isNotDefined } from 'utils'
import { promptAndSetEnvironment } from './utils' import { promptAndSetEnvironment } from './utils'
@ -125,13 +125,10 @@ const fixTypebots = async () => {
log: [{ emit: 'event', level: 'query' }, 'info', 'warn', 'error'], log: [{ emit: 'event', level: 'query' }, 'info', 'warn', 'error'],
}) })
const twoDaysAgo = new Date() const typebots = await prisma.publicTypebot.findMany({
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2)
const typebots = await prisma.typebot.findMany({
where: { where: {
updatedAt: { updatedAt: {
gte: twoDaysAgo, gte: new Date('2023-01-01T00:00:00.000Z'),
}, },
}, },
}) })
@ -150,7 +147,7 @@ const fixTypebots = async () => {
(progress / total) * 100 (progress / total) * 100
)}%) (${totalFixed} fixed typebots)` )}%) (${totalFixed} fixed typebots)`
) )
const parser = typebotSchema.safeParse({ const parser = publicTypebotSchema.safeParse({
...typebot, ...typebot,
updatedAt: new Date(typebot.updatedAt), updatedAt: new Date(typebot.updatedAt),
createdAt: new Date(typebot.createdAt), createdAt: new Date(typebot.createdAt),
@ -161,7 +158,7 @@ const fixTypebots = async () => {
updatedAt: new Date(), updatedAt: new Date(),
createdAt: new Date(typebot.createdAt), createdAt: new Date(typebot.createdAt),
} }
typebotSchema.parse(fixedTypebot) publicTypebotSchema.parse(fixedTypebot)
fixedTypebots.push(fixedTypebot) fixedTypebots.push(fixedTypebot)
totalFixed += 1 totalFixed += 1
diffs.push({ diffs.push({
@ -182,6 +179,13 @@ const fixTypebots = async () => {
where: { id: fixedTypebot.id }, where: { id: fixedTypebot.id },
data: { data: {
...fixedTypebot, ...fixedTypebot,
// theme: fixedTypebot.theme ?? undefined,
// settings: fixedTypebot.settings ?? undefined,
// resultsTablePreferences:
// 'resultsTablePreferences' in fixedTypebot &&
// fixedTypebot.resultsTablePreferences
// ? fixedTypebot.resultsTablePreferences
// : undefined,
} as any, } as any,
}) })
) )