feat(bubbles): ✨ Add image bubble
This commit is contained in:
@@ -344,3 +344,25 @@ export const GoogleAnalyticsLogo = (props: IconProps) => (
|
||||
</defs>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const GiphyLogo = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 163.79999999999998 35" {...props}>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M4 4h20v27H4z" fill="#000" />
|
||||
<g fillRule="nonzero">
|
||||
<path d="M0 3h4v29H0z" fill="#04ff8e" />
|
||||
<path d="M24 11h4v21h-4z" fill="#8e2eff" />
|
||||
<path d="M0 31h28v4H0z" fill="#00c5ff" />
|
||||
<path d="M0 0h16v4H0z" fill="#fff152" />
|
||||
<path d="M24 8V4h-4V0h-4v12h12V8" fill="#ff5b5b" />
|
||||
<path d="M24 16v-4h4" fill="#551c99" />
|
||||
</g>
|
||||
<path d="M16 0v4h-4" fill="#999131" />
|
||||
<path
|
||||
d="M59.1 12c-2-1.9-4.4-2.4-6.2-2.4-4.4 0-7.3 2.6-7.3 8 0 3.5 1.8 7.8 7.3 7.8 1.4 0 3.7-.3 5.2-1.4v-3.5h-6.9v-6h13.3v12.1c-1.7 3.5-6.4 5.3-11.7 5.3-10.7 0-14.8-7.2-14.8-14.3S42.7 3.2 52.9 3.2c3.8 0 7.1.8 10.7 4.4zm9.1 19.2V4h7.6v27.2zm20.1-7.4v7.3h-7.7V4h13.2c7.3 0 10.9 4.6 10.9 9.9 0 5.6-3.6 9.9-10.9 9.9zm0-6.5h5.5c2.1 0 3.2-1.6 3.2-3.3 0-1.8-1.1-3.4-3.2-3.4h-5.5zM125 31.2V20.9h-9.8v10.3h-7.7V4h7.7v10.3h9.8V4h7.6v27.2zm24.2-17.9l5.9-9.3h8.7v.3l-10.8 16v10.8h-7.7V20.3L135 4.3V4h8.7z"
|
||||
fill="#000"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ import { UploadIcon } from 'assets/icons'
|
||||
import { UploadButton } from 'components/shared/buttons/UploadButton'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { uploadFile } from 'services/utils'
|
||||
|
||||
export const PersonalInfoForm = () => {
|
||||
const {
|
||||
@@ -27,14 +26,10 @@ export const PersonalInfoForm = () => {
|
||||
isOAuthProvider,
|
||||
} = useUser()
|
||||
const [reloadParam, setReloadParam] = useState('')
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
|
||||
const handleFileChange = async (file: File) => {
|
||||
setIsUploading(true)
|
||||
const { url } = await uploadFile(file, `${user?.id}/avatar`)
|
||||
const handleFileUploaded = async (url: string) => {
|
||||
setReloadParam(Date.now().toString())
|
||||
updateUser({ image: url })
|
||||
setIsUploading(false)
|
||||
}
|
||||
|
||||
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -60,9 +55,9 @@ export const PersonalInfoForm = () => {
|
||||
<Stack>
|
||||
<UploadButton
|
||||
size="sm"
|
||||
filePath={`users/${user?.id}/avatar`}
|
||||
leftIcon={<UploadIcon />}
|
||||
isLoading={isUploading}
|
||||
onUploadChange={handleFileChange}
|
||||
onFileUploaded={handleFileUploaded}
|
||||
>
|
||||
Change photo
|
||||
</UploadButton>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FilterIcon,
|
||||
FlagIcon,
|
||||
GlobeIcon,
|
||||
ImageIcon,
|
||||
NumberIcon,
|
||||
PhoneIcon,
|
||||
TextIcon,
|
||||
@@ -29,6 +30,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
switch (type) {
|
||||
case BubbleStepType.TEXT:
|
||||
return <ChatIcon {...props} />
|
||||
case BubbleStepType.IMAGE:
|
||||
return <ImageIcon {...props} />
|
||||
case InputStepType.TEXT:
|
||||
return <TextIcon {...props} />
|
||||
case InputStepType.NUMBER:
|
||||
|
||||
@@ -16,6 +16,8 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
case InputStepType.TEXT: {
|
||||
return <Text>Text</Text>
|
||||
}
|
||||
case BubbleStepType.IMAGE:
|
||||
return <Text>Image</Text>
|
||||
case InputStepType.NUMBER: {
|
||||
return <Text>Number</Text>
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ export const StepTypesList = () => {
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
setRelativeCoordinates({ x, y })
|
||||
console.log({ x: rect.left, y: rect.top })
|
||||
setDraggedStepType(type)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Portal,
|
||||
PopoverContent,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
} from '@chakra-ui/react'
|
||||
import { ImagePopoverContent } from 'components/shared/ImageUploadContent'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
BubbleStep,
|
||||
BubbleStepType,
|
||||
ImageBubbleContent,
|
||||
ImageBubbleStep,
|
||||
TextBubbleStep,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
|
||||
type Props = {
|
||||
step: Exclude<BubbleStep, TextBubbleStep>
|
||||
}
|
||||
|
||||
export const ContentPopover = ({ step }: Props) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<PopoverContent onMouseDown={handleMouseDown} w="500px">
|
||||
<PopoverArrow />
|
||||
<PopoverBody ref={ref} shadow="lg">
|
||||
<StepContent step={step} />
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export const StepContent = ({ step }: Props) => {
|
||||
const { updateStep } = useTypebot()
|
||||
const handleContentChange = (content: ImageBubbleContent) =>
|
||||
updateStep(step.id, { content } as Partial<ImageBubbleStep>)
|
||||
|
||||
const handleNewImageSubmit = (url: string) => handleContentChange({ url })
|
||||
switch (step.type) {
|
||||
case BubbleStepType.IMAGE: {
|
||||
return (
|
||||
<ImagePopoverContent
|
||||
url={step.content?.url}
|
||||
onSubmit={handleNewImageSubmit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ContentPopover } from './ContentPopover'
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
LogicStepType,
|
||||
Step,
|
||||
StepOptions,
|
||||
TextBubbleStep,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
import {
|
||||
@@ -33,7 +34,7 @@ import { RedirectSettings } from './bodies/RedirectSettings'
|
||||
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
|
||||
|
||||
type Props = {
|
||||
step: Step
|
||||
step: Exclude<Step, TextBubbleStep>
|
||||
onExpandClick: () => void
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,10 @@ import {
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { DraggableStep, Step } from 'models'
|
||||
import { BubbleStep, DraggableStep, Step, TextBubbleStep } from 'models'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||
import {
|
||||
isInputStep,
|
||||
isLogicStep,
|
||||
isTextBubbleStep,
|
||||
isIntegrationStep,
|
||||
} from 'utils'
|
||||
import { isBubbleStep, isTextBubbleStep } from 'utils'
|
||||
import { Coordinates } from '@dnd-kit/core/dist/types'
|
||||
import { TextEditor } from './TextEditor/TextEditor'
|
||||
import { StepNodeContent } from './StepNodeContent'
|
||||
@@ -29,6 +24,7 @@ import { TargetEndpoint } from './TargetEndpoint'
|
||||
import { useRouter } from 'next/router'
|
||||
import { SettingsModal } from './SettingsPopoverContent/SettingsModal'
|
||||
import { StepSettings } from './SettingsPopoverContent/SettingsPopoverContent'
|
||||
import { ContentPopover } from './ContentPopover'
|
||||
|
||||
export const StepNode = ({
|
||||
step,
|
||||
@@ -185,15 +181,16 @@ export const StepNode = ({
|
||||
}}
|
||||
pos="absolute"
|
||||
right="15px"
|
||||
top="18px"
|
||||
bottom="18px"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{hasPopover(step) && (
|
||||
{hasSettingsPopover(step) && (
|
||||
<SettingsPopoverContent step={step} onExpandClick={onModalOpen} />
|
||||
)}
|
||||
{hasContentPopover(step) && <ContentPopover step={step} />}
|
||||
<SettingsModal isOpen={isModalOpen} onClose={onModalClose}>
|
||||
<StepSettings step={step} />
|
||||
</SettingsModal>
|
||||
@@ -203,5 +200,10 @@ export const StepNode = ({
|
||||
)
|
||||
}
|
||||
|
||||
const hasPopover = (step: Step) =>
|
||||
isInputStep(step) || isLogicStep(step) || isIntegrationStep(step)
|
||||
const hasSettingsPopover = (step: Step): step is Exclude<Step, BubbleStep> =>
|
||||
!isBubbleStep(step)
|
||||
|
||||
const hasContentPopover = (
|
||||
step: Step
|
||||
): step is Exclude<BubbleStep, TextBubbleStep> =>
|
||||
isBubbleStep(step) && !isTextBubbleStep(step)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex, HStack, Stack, Tag, Text } from '@chakra-ui/react'
|
||||
import { Box, Flex, HStack, Image, Stack, Tag, Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
Step,
|
||||
@@ -34,6 +34,20 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.IMAGE: {
|
||||
return !step.content?.url ? (
|
||||
<Text color={'gray.500'}>Click to edit...</Text>
|
||||
) : (
|
||||
<Box w="full">
|
||||
<Image
|
||||
src={step.content?.url}
|
||||
alt="Step image"
|
||||
rounded="md"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
case InputStepType.TEXT: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { BaseSelection, createEditor, Transforms } from 'slate'
|
||||
import { ToolBar } from './ToolBar'
|
||||
import { parseHtmlStringToPlainText } from 'services/utils'
|
||||
import { TextStep, Variable } from 'models'
|
||||
import { TextBubbleStep, Variable } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { ReactEditor } from 'slate-react'
|
||||
|
||||
@@ -87,7 +87,7 @@ export const TextEditor = ({
|
||||
richText: value,
|
||||
plainText: parseHtmlStringToPlainText(html),
|
||||
},
|
||||
} as TextStep)
|
||||
} as TextBubbleStep)
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Flex, Input, Stack } from '@chakra-ui/react'
|
||||
import { GiphyFetch } from '@giphy/js-fetch-api'
|
||||
import { Grid, SearchContext } from '@giphy/react-components'
|
||||
import { GiphyLogo } from 'assets/logos'
|
||||
import React, { useContext, useState, useEffect } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type GiphySearchProps = {
|
||||
onSubmit: (url: string) => void
|
||||
}
|
||||
|
||||
const giphyFetch = new GiphyFetch(
|
||||
process.env.NEXT_PUBLIC_GIPHY_API_KEY as string
|
||||
)
|
||||
|
||||
export const GiphySearch = ({ onSubmit }: GiphySearchProps) => {
|
||||
const { fetchGifs, searchKey, setSearch } = useContext(SearchContext)
|
||||
const fetchGifsTrending = (offset: number) =>
|
||||
giphyFetch.trending({ offset, limit: 10 })
|
||||
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [debouncedInputValue] = useDebounce(inputValue, 300)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedInputValue === '') return
|
||||
setSearch(debouncedInputValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedInputValue])
|
||||
|
||||
const updateUrl = (url: string) => {
|
||||
onSubmit(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex align="center">
|
||||
<Input
|
||||
flex="1"
|
||||
autoFocus
|
||||
placeholder="Search..."
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
value={inputValue}
|
||||
/>
|
||||
<GiphyLogo w="100px" />
|
||||
</Flex>
|
||||
<Flex overflowY="scroll" maxH="400px">
|
||||
<Grid
|
||||
onGifClick={(gif, e) => {
|
||||
e.preventDefault()
|
||||
updateUrl(gif.images.downsized.url)
|
||||
}}
|
||||
key={searchKey}
|
||||
fetchGifs={searchKey === '' ? fetchGifsTrending : fetchGifs}
|
||||
width={475}
|
||||
columns={3}
|
||||
className="my-4"
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { ChangeEvent, FormEvent, useState } from 'react'
|
||||
import { Button, HStack, Input, Stack } from '@chakra-ui/react'
|
||||
import { SearchContextManager } from '@giphy/react-components'
|
||||
import { UploadButton } from '../buttons/UploadButton'
|
||||
import { GiphySearch } from './GiphySearch'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
onSubmit: (url: string) => void
|
||||
}
|
||||
|
||||
export const ImageUploadContent = ({ url, onSubmit }: Props) => {
|
||||
const [currentTab, setCurrentTab] = useState<'link' | 'upload' | 'giphy'>(
|
||||
'upload'
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<HStack>
|
||||
<Button
|
||||
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('upload')}
|
||||
size="sm"
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentTab === 'link' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('link')}
|
||||
size="sm"
|
||||
>
|
||||
Embed link
|
||||
</Button>
|
||||
{process.env.NEXT_PUBLIC_GIPHY_API_KEY && (
|
||||
<Button
|
||||
variant={currentTab === 'giphy' ? 'solid' : 'ghost'}
|
||||
onClick={() => setCurrentTab('giphy')}
|
||||
size="sm"
|
||||
>
|
||||
Giphy
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<BodyContent tab={currentTab} onSubmit={onSubmit} url={url} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const BodyContent = ({
|
||||
tab,
|
||||
url,
|
||||
onSubmit,
|
||||
}: {
|
||||
tab: 'upload' | 'link' | 'giphy'
|
||||
url?: string
|
||||
onSubmit: (url: string) => void
|
||||
}) => {
|
||||
switch (tab) {
|
||||
case 'upload':
|
||||
return <UploadFileContent onNewUrl={onSubmit} />
|
||||
case 'link':
|
||||
return <EmbedLinkContent initialUrl={url} onNewUrl={onSubmit} />
|
||||
case 'giphy':
|
||||
return <GiphyContent onNewUrl={onSubmit} />
|
||||
}
|
||||
}
|
||||
|
||||
type ContentProps = { initialUrl?: string; onNewUrl: (url: string) => void }
|
||||
|
||||
const UploadFileContent = ({ onNewUrl }: ContentProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
return (
|
||||
<Stack>
|
||||
<UploadButton
|
||||
filePath={`typebots/${typebot?.id}`}
|
||||
onFileUploaded={onNewUrl}
|
||||
includeFileName
|
||||
colorScheme="blue"
|
||||
>
|
||||
Choose an image
|
||||
</UploadButton>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => {
|
||||
const [imageUrl, setImageUrl] = useState(initialUrl ?? '')
|
||||
|
||||
const handleImageUrlChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
setImageUrl(e.target.value)
|
||||
|
||||
const handleUrlSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
onNewUrl(imageUrl)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack as="form" onSubmit={handleUrlSubmit}>
|
||||
<Input
|
||||
placeholder={'Paste the image link...'}
|
||||
onChange={handleImageUrlChange}
|
||||
value={imageUrl}
|
||||
/>
|
||||
<Button type="submit" disabled={imageUrl === ''} colorScheme="blue">
|
||||
Embed image
|
||||
</Button>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const GiphyContent = ({ onNewUrl }: ContentProps) => (
|
||||
<SearchContextManager
|
||||
apiKey={process.env.NEXT_PUBLIC_GIPHY_API_KEY as string}
|
||||
>
|
||||
<GiphySearch onSubmit={onNewUrl} />
|
||||
</SearchContextManager>
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export { ImageUploadContent as ImagePopoverContent } from './ImageUploadContent'
|
||||
@@ -1,19 +1,37 @@
|
||||
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
|
||||
import React, { ChangeEvent } from 'react'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { compressFile, uploadFile } from 'services/utils'
|
||||
|
||||
type UploadButtonProps = { onUploadChange: (file: File) => void } & ButtonProps
|
||||
type UploadButtonProps = {
|
||||
filePath: string
|
||||
includeFileName?: boolean
|
||||
onFileUploaded: (url: string) => void
|
||||
} & ButtonProps
|
||||
|
||||
export const UploadButton = ({
|
||||
onUploadChange,
|
||||
filePath,
|
||||
includeFileName,
|
||||
onFileUploaded,
|
||||
...props
|
||||
}: UploadButtonProps) => {
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
|
||||
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target?.files) return
|
||||
onUploadChange(e.target.files[0])
|
||||
setIsUploading(true)
|
||||
const file = e.target.files[0]
|
||||
const { url } = await uploadFile(
|
||||
await compressFile(file),
|
||||
filePath + (includeFileName ? `/${file.name}` : '')
|
||||
)
|
||||
if (url) onFileUploaded(url)
|
||||
setIsUploading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<chakra.input
|
||||
data-testid="file-upload-input"
|
||||
type="file"
|
||||
id="file-input"
|
||||
display="none"
|
||||
@@ -25,6 +43,7 @@ export const UploadButton = ({
|
||||
size="sm"
|
||||
htmlFor="file-input"
|
||||
cursor="pointer"
|
||||
isLoading={isUploading}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { userIds } from 'cypress/plugins/data'
|
||||
|
||||
describe('Dashboard page', () => {
|
||||
describe('Account page', () => {
|
||||
before(() => {
|
||||
cy.intercept({
|
||||
url: 'https://s3.eu-west-3.amazonaws.com/typebot',
|
||||
@@ -35,7 +35,7 @@ describe('Dashboard page', () => {
|
||||
.should('have.attr', 'src')
|
||||
.should(
|
||||
'include',
|
||||
`https://s3.eu-west-3.amazonaws.com/typebot/${userIds[0]}/avatar`
|
||||
`https://s3.eu-west-3.amazonaws.com/typebot/users/${userIds[0]}/avatar`
|
||||
)
|
||||
cy.findByRole('button', { name: 'Save' }).should('exist').click()
|
||||
cy.wait('@getUpdatedSession')
|
||||
@@ -45,7 +45,7 @@ describe('Dashboard page', () => {
|
||||
.should('have.attr', 'src')
|
||||
.should(
|
||||
'include',
|
||||
`https://s3.eu-west-3.amazonaws.com/typebot/${userIds[0]}/avatar`
|
||||
`https://s3.eu-west-3.amazonaws.com/typebot/users/${userIds[0]}/avatar`
|
||||
)
|
||||
cy.findByRole('button', { name: 'Save' }).should('not.exist')
|
||||
})
|
||||
|
||||
86
apps/builder/cypress/tests/bubbles/image.ts
Normal file
86
apps/builder/cypress/tests/bubbles/image.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||
import { getIframeBody } from 'cypress/support'
|
||||
import { BubbleStepType, Step } from 'models'
|
||||
|
||||
const unsplashImageSrc =
|
||||
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
|
||||
|
||||
describe('Image bubbles', () => {
|
||||
before(() => {
|
||||
cy.intercept({
|
||||
url: 'https://s3.eu-west-3.amazonaws.com/typebot',
|
||||
method: 'POST',
|
||||
}).as('postImage')
|
||||
})
|
||||
afterEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
})
|
||||
})
|
||||
describe('Content settings', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.IMAGE,
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.signOut()
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
cy.findByText('Click to edit...').click()
|
||||
})
|
||||
|
||||
it('upload image file correctly', () => {
|
||||
cy.findByTestId('file-upload-input').attachFile('avatar.jpg')
|
||||
cy.wait('@postImage')
|
||||
cy.findByRole('img')
|
||||
.should('have.attr', 'src')
|
||||
.should(
|
||||
'include',
|
||||
`https://s3.eu-west-3.amazonaws.com/typebot/typebots/typebot3/avatar.jpg`
|
||||
)
|
||||
})
|
||||
|
||||
it('should import image links correctly', () => {
|
||||
cy.findByRole('button', { name: 'Embed link' }).click()
|
||||
cy.findByPlaceholderText('Paste the image link...')
|
||||
.clear()
|
||||
.type(unsplashImageSrc)
|
||||
cy.findByRole('button', { name: 'Embed image' }).click()
|
||||
cy.findByRole('img')
|
||||
.should('have.attr', 'src')
|
||||
.should('include', unsplashImageSrc)
|
||||
})
|
||||
|
||||
it.only('should import giphy gifs correctly', () => {
|
||||
cy.findByRole('button', { name: 'Giphy' }).click()
|
||||
cy.findAllByRole('img').eq(3).click()
|
||||
cy.findAllByRole('img')
|
||||
.first()
|
||||
.should('have.attr', 'src')
|
||||
.should('contain', `giphy.com/media`)
|
||||
})
|
||||
})
|
||||
describe('Preview', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.IMAGE,
|
||||
content: {
|
||||
url: unsplashImageSrc,
|
||||
},
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.signOut()
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot3/edit')
|
||||
})
|
||||
|
||||
it('should display correctly', () => {
|
||||
cy.findByRole('button', { name: 'Preview' }).click()
|
||||
getIframeBody()
|
||||
.findByRole('img')
|
||||
.should('have.attr', 'src')
|
||||
.should('eq', unsplashImageSrc)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,43 +1,15 @@
|
||||
import { userIds } from 'cypress/plugins/data'
|
||||
import {
|
||||
parseTestTypebot,
|
||||
preventUserFromRefreshing,
|
||||
} from 'cypress/plugins/utils'
|
||||
import { BubbleStepType } from 'models'
|
||||
import { createTypebotWithStep } from 'cypress/plugins/data'
|
||||
import { preventUserFromRefreshing } from 'cypress/plugins/utils'
|
||||
import { getIframeBody } from 'cypress/support'
|
||||
import { BubbleStepType, Step } from 'models'
|
||||
|
||||
describe('Text bubbles', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
cy.task(
|
||||
'createTypebot',
|
||||
parseTestTypebot({
|
||||
id: 'typebot3',
|
||||
name: 'Typebot #3',
|
||||
ownerId: userIds[1],
|
||||
steps: {
|
||||
byId: {
|
||||
step1: {
|
||||
id: 'step1',
|
||||
blockId: 'block1',
|
||||
type: BubbleStepType.TEXT,
|
||||
content: { html: '', plainText: '', richText: [] },
|
||||
},
|
||||
},
|
||||
allIds: ['step1'],
|
||||
},
|
||||
blocks: {
|
||||
byId: {
|
||||
block1: {
|
||||
id: 'block1',
|
||||
graphCoordinates: { x: 400, y: 200 },
|
||||
title: 'Block #1',
|
||||
stepIds: ['step1'],
|
||||
},
|
||||
},
|
||||
allIds: ['block1'],
|
||||
},
|
||||
})
|
||||
)
|
||||
createTypebotWithStep({
|
||||
type: BubbleStepType.TEXT,
|
||||
content: { html: '', plainText: '', richText: [] },
|
||||
} as Omit<Step, 'id' | 'blockId'>)
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
@@ -78,11 +50,3 @@ describe('Text bubbles', () => {
|
||||
.should('contain.text', 'Underlined text')
|
||||
})
|
||||
})
|
||||
|
||||
const getIframeBody = () => {
|
||||
return cy
|
||||
.get('#typebot-iframe')
|
||||
.its('0.contentDocument.body')
|
||||
.should('not.be.empty')
|
||||
.then(cy.wrap)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from 'path'
|
||||
import { parse } from 'papaparse'
|
||||
|
||||
describe('ResultsPage', () => {
|
||||
describe('Results page', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept({ url: '/api/typebots/typebot2/results*', method: 'GET' }).as(
|
||||
'getResults'
|
||||
@@ -35,7 +35,7 @@ describe('ResultsPage', () => {
|
||||
cy.findByText('content50').should('not.exist')
|
||||
cy.findByText('content199').should('exist')
|
||||
cy.findByTestId('table-wrapper').scrollTo('bottom')
|
||||
cy.findByText('content149').should('exist')
|
||||
cy.findByText('content149', { timeout: 10000 }).should('exist')
|
||||
cy.findByTestId('table-wrapper').scrollTo('bottom')
|
||||
cy.findByText('content99').should('exist')
|
||||
cy.findByTestId('table-wrapper').scrollTo('bottom')
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"@dnd-kit/sortable": "^5.1.0",
|
||||
"@emotion/react": "^11.7.1",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@giphy/js-fetch-api": "^4.1.2",
|
||||
"@giphy/js-types": "^4.1.0",
|
||||
"@giphy/react-components": "^5.4.0",
|
||||
"@googleapis/drive": "^2.1.0",
|
||||
"@next-auth/prisma-adapter": "next",
|
||||
"@udecode/plate-basic-marks": "^9.0.0",
|
||||
@@ -26,6 +29,7 @@
|
||||
"@udecode/plate-ui-toolbar": "^9.0.0",
|
||||
"aws-sdk": "^2.1051.0",
|
||||
"bot-engine": "*",
|
||||
"browser-image-compression": "^1.0.17",
|
||||
"db": "*",
|
||||
"fast-equals": "^2.0.4",
|
||||
"focus-visible": "^5.2.0",
|
||||
|
||||
@@ -8,39 +8,35 @@ const handler = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<void> => {
|
||||
try {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
if (req.method === 'GET') {
|
||||
const session = await getSession({ req })
|
||||
if (!session) {
|
||||
res.status(401)
|
||||
return
|
||||
}
|
||||
aws.config.update({
|
||||
accessKeyId: process.env.S3_UPLOAD_KEY,
|
||||
secretAccessKey: process.env.S3_UPLOAD_SECRET,
|
||||
region: process.env.S3_UPLOAD_REGION,
|
||||
signatureVersion: 'v4',
|
||||
})
|
||||
|
||||
const s3 = new aws.S3()
|
||||
const post = s3.createPresignedPost({
|
||||
Bucket: process.env.S3_UPLOAD_BUCKET,
|
||||
Fields: {
|
||||
ACL: 'public-read',
|
||||
key: req.query.key,
|
||||
'Content-Type': req.query.fileType,
|
||||
},
|
||||
Expires: 120, // seconds
|
||||
Conditions: [['content-length-range', 0, maxUploadFileSize]],
|
||||
})
|
||||
|
||||
return res.status(200).json(post)
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
if (req.method === 'GET') {
|
||||
const session = await getSession({ req })
|
||||
if (!session) {
|
||||
res.status(401)
|
||||
return
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
aws.config.update({
|
||||
accessKeyId: process.env.S3_UPLOAD_KEY,
|
||||
secretAccessKey: process.env.S3_UPLOAD_SECRET,
|
||||
region: process.env.S3_UPLOAD_REGION,
|
||||
signatureVersion: 'v4',
|
||||
})
|
||||
|
||||
const s3 = new aws.S3()
|
||||
const post = s3.createPresignedPost({
|
||||
Bucket: process.env.S3_UPLOAD_BUCKET,
|
||||
Fields: {
|
||||
ACL: 'public-read',
|
||||
key: req.query.key,
|
||||
'Content-Type': req.query.fileType,
|
||||
},
|
||||
Expires: 120, // seconds
|
||||
Conditions: [['content-length-range', 0, maxUploadFileSize]],
|
||||
})
|
||||
|
||||
return res.status(200).json(post)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Block,
|
||||
TextStep,
|
||||
TextBubbleStep,
|
||||
PublicTypebot,
|
||||
BackgroundType,
|
||||
Settings,
|
||||
@@ -119,7 +119,7 @@ export const parseNewStep = (
|
||||
const id = `s${shortId.generate()}`
|
||||
switch (type) {
|
||||
case BubbleStepType.TEXT: {
|
||||
const textStep: Pick<TextStep, 'type' | 'content'> = {
|
||||
const textStep: Pick<TextBubbleStep, 'type' | 'content'> = {
|
||||
type,
|
||||
content: { html: '', richText: [], plainText: '' },
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import imageCompression from 'browser-image-compression'
|
||||
import { Parser } from 'htmlparser2'
|
||||
import { Step } from 'models'
|
||||
|
||||
@@ -78,6 +79,16 @@ export const uploadFile = async (file: File, key: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const compressFile = async (file: File) => {
|
||||
const options = {
|
||||
maxSizeMB: 0.5,
|
||||
maxWidthOrHeight: 1600,
|
||||
}
|
||||
return ['image/jpg', 'image/jpeg', 'image/png'].includes(file.type)
|
||||
? imageCompression(file, options)
|
||||
: file
|
||||
}
|
||||
|
||||
export const removeUndefinedFields = <T>(obj: T): T =>
|
||||
Object.keys(obj).reduce(
|
||||
(acc, key) =>
|
||||
|
||||
@@ -13,7 +13,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
column: req.query['referenceCell[column]'],
|
||||
value: req.query['referenceCell[value]'],
|
||||
} as Cell
|
||||
console.log(referenceCell)
|
||||
const extractingColumns = req.query.columns as string[]
|
||||
const doc = new GoogleSpreadsheet(spreadsheetId)
|
||||
doc.useOAuth2Client(await getAuthenticatedGoogleClient(credentialsId))
|
||||
|
||||
Reference in New Issue
Block a user