@ -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
|
||||||
|
@ -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 "{inputValue}"
|
>
|
||||||
</Button>
|
Create "{inputValue}"
|
||||||
)}
|
</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>
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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'}
|
||||||
|
@ -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 -
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
|
26
apps/builder/src/hooks/useOutsideClick.ts
Normal file
26
apps/builder/src/hooks/useOutsideClick.ts
Normal 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)
|
||||||
|
}
|
@ -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,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user