Skip to content

Commit

Permalink
⚡ (s3) Improve storage management and type safety
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno authored and jmgoncalves97 committed Jan 17, 2025
1 parent 7fcafef commit cd50ada
Show file tree
Hide file tree
Showing 47 changed files with 790 additions and 128 deletions.
2 changes: 0 additions & 2 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
"libphonenumber-js": "1.10.37",
"micro": "10.0.1",
"micro-cors": "0.1.1",
"minio": "7.1.1",
"next": "13.4.3",
"next-auth": "4.22.1",
"next-international": "0.9.5",
Expand Down Expand Up @@ -106,7 +105,6 @@
"@types/canvas-confetti": "1.6.0",
"@types/jsonwebtoken": "9.0.2",
"@types/micro-cors": "0.1.3",
"@types/minio": "7.1.1",
"@types/node": "20.4.2",
"@types/nodemailer": "6.4.8",
"@types/nprogress": "0.2.0",
Expand Down
7 changes: 4 additions & 3 deletions apps/builder/src/components/EditableEmojiOrImageIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import {
import React from 'react'
import { EmojiOrImageIcon } from './EmojiOrImageIcon'
import { ImageUploadContent } from './ImageUploadContent'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'

type Props = {
uploadFilePath: string
uploadFileProps: FilePathUploadProps
icon?: string | null
onChangeIcon: (icon: string) => void
boxSize?: string
}

export const EditableEmojiOrImageIcon = ({
uploadFilePath,
uploadFileProps,
icon,
onChangeIcon,
boxSize,
Expand Down Expand Up @@ -54,7 +55,7 @@ export const EditableEmojiOrImageIcon = ({
</Tooltip>
<PopoverContent p="2">
<ImageUploadContent
filePath={uploadFilePath}
uploadFileProps={uploadFileProps}
defaultUrl={icon ?? ''}
onSubmit={onChangeIcon}
excludedTabs={['giphy', 'unsplash']}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { TextInput } from '../inputs/TextInput'
import { EmojiSearchableList } from './emoji/EmojiSearchableList'
import { UnsplashPicker } from './UnsplashPicker'
import { IconPicker } from './IconPicker'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'

type Tabs = 'link' | 'upload' | 'giphy' | 'emoji' | 'unsplash' | 'icon'

type Props = {
filePath: string | undefined
includeFileName?: boolean
uploadFileProps: FilePathUploadProps | undefined
defaultUrl?: string
imageSize?: 'small' | 'regular' | 'thumb'
initialTab?: Tabs
Expand All @@ -36,8 +36,7 @@ const defaultDisplayedTabs: Tabs[] = [
]

export const ImageUploadContent = ({
filePath,
includeFileName,
uploadFileProps,
defaultUrl,
onSubmit,
imageSize = 'regular',
Expand Down Expand Up @@ -123,8 +122,7 @@ export const ImageUploadContent = ({
</HStack>

<BodyContent
filePath={filePath}
includeFileName={includeFileName}
uploadFileProps={uploadFileProps}
tab={currentTab}
imageSize={imageSize}
onSubmit={handleSubmit}
Expand All @@ -135,27 +133,24 @@ export const ImageUploadContent = ({
}

const BodyContent = ({
includeFileName,
filePath,
uploadFileProps,
tab,
defaultUrl,
imageSize,
onSubmit,
}: {
includeFileName?: boolean
filePath: string | undefined
uploadFileProps?: FilePathUploadProps
tab: Tabs
defaultUrl?: string
imageSize: 'small' | 'regular' | 'thumb'
onSubmit: (url: string) => void
}) => {
switch (tab) {
case 'upload': {
if (!filePath) return null
if (!uploadFileProps) return null
return (
<UploadFileContent
filePath={filePath}
includeFileName={includeFileName}
uploadFileProps={uploadFileProps}
onNewUrl={onSubmit}
/>
)
Expand All @@ -176,16 +171,14 @@ const BodyContent = ({
type ContentProps = { onNewUrl: (url: string) => void }

const UploadFileContent = ({
filePath,
includeFileName,
uploadFileProps,
onNewUrl,
}: ContentProps & { filePath: string; includeFileName?: boolean }) => (
}: ContentProps & { uploadFileProps: FilePathUploadProps }) => (
<Flex justify="center" py="2">
<UploadButton
fileType="image"
filePath={filePath}
filePathProps={uploadFileProps}
onFileUploaded={onNewUrl}
includeFileName={includeFileName}
colorScheme="blue"
>
Choose an image
Expand Down
42 changes: 28 additions & 14 deletions apps/builder/src/components/ImageUploadContent/UploadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,56 @@
import { useToast } from '@/hooks/useToast'
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
import { ChangeEvent, useState } from 'react'
import { uploadFiles } from '@typebot.io/lib/s3/uploadFiles'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
import { trpc } from '@/lib/trpc'
import { compressFile } from '@/helpers/compressFile'

type UploadButtonProps = {
fileType: 'image' | 'audio'
filePath: string
includeFileName?: boolean
filePathProps: FilePathUploadProps
onFileUploaded: (url: string) => void
} & ButtonProps

export const UploadButton = ({
fileType,
filePath,
includeFileName,
filePathProps,
onFileUploaded,
...props
}: UploadButtonProps) => {
const [isUploading, setIsUploading] = useState(false)
const { showToast } = useToast()
const [file, setFile] = useState<File>()

const { mutate } = trpc.generateUploadUrl.useMutation({
onSettled: () => {
setIsUploading(false)
},
onSuccess: async (data) => {
const upload = await fetch(data.presignedUrl, {
method: 'PUT',
body: file,
})

if (!upload.ok) {
showToast({ description: 'Error while trying to upload the file.' })
return
}

onFileUploaded(data.fileUrl + '?v=' + Date.now())
},
})

const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target?.files) return
setIsUploading(true)
const file = e.target.files[0] as File | undefined
if (!file)
return showToast({ description: 'Could not read file.', status: 'error' })
const urls = await uploadFiles({
files: [
{
file: await compressFile(file),
path: `public/${filePath}${includeFileName ? `/${file.name}` : ''}`,
},
],
setFile(await compressFile(file))
mutate({
filePathProps,
fileType: file.type,
})
if (urls.length && urls[0]) onFileUploaded(urls[0] + '?v=' + Date.now())
setIsUploading(false)
}

return (
Expand Down
23 changes: 14 additions & 9 deletions apps/builder/src/features/account/components/MyAccountForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,20 @@ export const MyAccountForm = () => {
name={user?.name ?? undefined}
/>
<Stack>
<UploadButton
size="sm"
fileType="image"
filePath={`users/${user?.id}/avatar`}
leftIcon={<UploadIcon />}
onFileUploaded={handleFileUploaded}
>
{scopedT('changePhotoButton.label')}
</UploadButton>
{user?.id && (
<UploadButton
size="sm"
fileType="image"
filePathProps={{
userId: user.id,
fileName: 'avatar',
}}
leftIcon={<UploadIcon />}
onFileUploaded={handleFileUploaded}
>
{scopedT('changePhotoButton.label')}
</UploadButton>
)}
<Text color="gray.500" fontSize="sm">
{scopedT('changePhotoButton.specification')}
</Text>
Expand Down
11 changes: 9 additions & 2 deletions apps/builder/src/features/blocks/bubbles/audio/audio.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseH
import { BubbleBlockType, defaultAudioBubbleContent } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { proWorkspaceId } from '@typebot.io/lib/playwright/databaseSetup'

const audioSampleUrl =
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
Expand All @@ -30,11 +31,17 @@ test('should work as expected', async ({ page }) => {
await page.setInputFiles('input[type="file"]', getTestAsset('sample.mp3'))
await expect(page.locator('audio')).toHaveAttribute(
'src',
RegExp(`/public/typebots/${typebotId}/blocks`, 'gm')
RegExp(
`/public/workspaces/${proWorkspaceId}/typebots/${typebotId}/blocks`,
'gm'
)
)
await page.getByRole('button', { name: 'Preview', exact: true }).click()
await expect(page.locator('audio')).toHaveAttribute(
'src',
RegExp(`/public/typebots/${typebotId}/blocks`, 'gm')
RegExp(
`/public/workspaces/${proWorkspaceId}/typebots/${typebotId}/blocks`,
'gm'
)
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import { useState } from 'react'
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { useScopedI18n } from '@/locales'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'

type Props = {
fileUploadPath: string
uploadFileProps: FilePathUploadProps
content: AudioBubbleContent
onContentChange: (content: AudioBubbleContent) => void
}

export const AudioBubbleForm = ({
fileUploadPath,
uploadFileProps,
content,
onContentChange,
}: Props) => {
Expand Down Expand Up @@ -49,7 +50,7 @@ export const AudioBubbleForm = ({
<Flex justify="center" py="2">
<UploadButton
fileType="audio"
filePath={fileUploadPath}
filePathProps={uploadFileProps}
onFileUploaded={updateUrl}
colorScheme="blue"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { ImageUploadContent } from '@/components/ImageUploadContent'
import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
import { useScopedI18n } from '@/locales'
import { Stack } from '@chakra-ui/react'
import { isDefined, isNotEmpty } from '@typebot.io/lib'
import { ImageBubbleBlock } from '@typebot.io/schemas'
import React, { useState } from 'react'

type Props = {
typebotId: string
uploadFileProps: FilePathUploadProps
block: ImageBubbleBlock
onContentChange: (content: ImageBubbleBlock['content']) => void
}

export const ImageBubbleSettings = ({
typebotId,
uploadFileProps,
block,
onContentChange,
}: Props) => {
Expand Down Expand Up @@ -53,7 +54,7 @@ export const ImageBubbleSettings = ({
return (
<Stack p="2" spacing={4}>
<ImageUploadContent
filePath={`typebots/${typebotId}/blocks/${block.id}`}
uploadFileProps={uploadFileProps}
defaultUrl={block.content?.url}
onSubmit={updateImage}
/>
Expand Down
6 changes: 5 additions & 1 deletion apps/builder/src/features/blocks/bubbles/image/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseH
import { BubbleBlockType, defaultImageBubbleContent } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { proWorkspaceId } from '@typebot.io/lib/playwright/databaseSetup'

const unsplashImageSrc =
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
Expand All @@ -29,7 +30,10 @@ test.describe.parallel('Image bubble block', () => {
await page.setInputFiles('input[type="file"]', getTestAsset('avatar.jpg'))
await expect(page.locator('img')).toHaveAttribute(
'src',
new RegExp(`/public/typebots/${typebotId}/blocks/block2`, 'gm')
new RegExp(
`/public/workspaces/${proWorkspaceId}/typebots/${typebotId}/blocks/block2`,
'gm'
)
)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const PictureChoiceItemNode = ({
>
{typebot && (
<PictureChoiceItemSettings
workspaceId={typebot.workspaceId}
typebotId={typebot.id}
item={item}
blockId={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import { ConditionForm } from '@/features/blocks/logic/condition/components/Cond
import { Condition, LogicalOperator } from '@typebot.io/schemas'

type Props = {
workspaceId: string
typebotId: string
blockId: string
item: PictureChoiceItem
onItemChange: (updates: Partial<PictureChoiceItem>) => void
}

export const PictureChoiceItemSettings = ({
workspaceId,
typebotId,
blockId,
item,
Expand Down Expand Up @@ -69,7 +71,12 @@ export const PictureChoiceItemSettings = ({
</PopoverTrigger>
<PopoverContent p="4" w="500px">
<ImageUploadContent
filePath={`typebots/${typebotId}/blocks/${blockId}/items/${item.id}`}
uploadFileProps={{
workspaceId,
typebotId,
blockId,
itemId: item.id,
}}
defaultUrl={item.pictureSrc}
onSubmit={(url) => {
updateImage(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { openAICredentialsSchema } from '@typebot.io/schemas/features/blocks/int
import { smtpCredentialsSchema } from '@typebot.io/schemas/features/blocks/integrations/sendEmail'
import { encrypt } from '@typebot.io/lib/api/encryption'
import { z } from 'zod'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden copy'
import { whatsAppCredentialsSchema } from '@typebot.io/schemas/features/whatsapp'
import { Credentials } from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib/utils'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'

const inputShape = {
data: true,
Expand Down
Loading

0 comments on commit cd50ada

Please sign in to comment.